{"id":"ee8ad504-1442-4e23-8fb6-600d15494500","entityType":"agent","slug":"clawhub-wyuc-openmaic","name":"OpenMAIC","canonicalUrl":"https://www.xpersona.co/agent/clawhub-wyuc-openmaic","canonicalPath":"/agent/clawhub-wyuc-openmaic","generatedAt":"2026-10-09T14:59:27.564Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T03:25:13.136Z","emptyReason":null},"description":"OpenMAIC assistant for setting up, generating, and extending OpenMAIC. Use when the user wants to use OpenMAIC, generate a multi-agent interactive classroom, or build on / extend / customize OpenMAIC and its @openmaic/* SDK (secondary development, 二开) — covers Live Demo or local setup, startup modes, provider keys, classroom generation, and secondary development (forking, providers/storage/themes, routes, or the renderer/editor).","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 6.4K downloads reported by the source. Last updated 10/9/2026.","installCommand":"clawhub skill install s177bd906czjkr4f79ahq4wj8183m0w4:openmaic","sourceUrl":"https://clawhub.ai/wyuc/openmaic","homepage":"https://clawhub.ai/wyuc/skills/openmaic","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/wyuc/openmaic","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/wyuc/skills/openmaic","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":69,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"OpenMAIC technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-09T03:25:13.136Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T03:25:13.136Z","emptyReason":null},"stars":null,"forks":null,"downloads":6377,"packageName":null,"latestVersion":"0.3.11","tractionLabel":"6.4K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T03:25:13.136Z","emptyReason":null},"lastUpdatedAt":"2026-10-09T03:25:13.136Z","lastCrawledAt":"2026-10-09T03:25:13.136Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-10T03:25:13.136Z","lastVerifiedAt":null,"highlights":[{"version":"0.3.11","createdAt":"2026-10-04T14:41:35.081Z","changelog":"- Updated the classroom generation phase to clarify retry behavior on failed jobs (now references retrying failed jobs per the generate flow). - Minor clarification in the classroom generation step about submitting supported fields and the handling of optional features. - Removed obsolete documentation file skill-card.md. - No changes to core setup or workflow phases.","fileCount":11,"zipByteSize":29344},{"version":"0.3.10","createdAt":"2026-10-02T12:13:36.908Z","changelog":"openmaic 0.3.10 - Improved provider configuration instructions—users now update `openmaic.yml`, `.env.local`, or web app settings, instead of legacy config files. - Clarified that all provider/model selection is controlled solely by OpenMAIC server-side config; request-time overrides are not supported. - Enhanced classroom generation: only permitted fields (`requirement`, `materialIds`) are submitted, matching server config for optional features. - Updated confirmation requirements for file uploads; confirmation is required before reading and uploading any local files. - Response instructions and phase explanations have been refined for clarity, especially regarding Live Demo and extension/SDK workflows. - Removed outdated references and files, including skill-card.md, reflecting the revised setup flow.","fileCount":11,"zipByteSize":27397},{"version":"0.3.9","createdAt":"2026-09-29T09:52:00.439Z","changelog":"OpenMAIC v0.3.9 - Updated content in references/extend-cookbook.md and references/startup-modes.md for improved clarity and guidance. - Removed the unused skill-card.md file. - No structural or workflow changes—functionality and rules remain as previously documented.","fileCount":11,"zipByteSize":23339},{"version":"0.3.8","createdAt":"2026-09-28T03:55:20.573Z","changelog":"openmaic 0.3.8 - Updated documentation: references/extend-cookbook.md revised for improved secondary development guidance. - Removed skill-card.md to streamline documentation. - No changes to core functionality—this is a docs/maintenance release.","fileCount":11,"zipByteSize":22818},{"version":"0.3.7","createdAt":"2026-09-23T05:26:50.410Z","changelog":"- Removed the unused skill-card.md file. - Updated generate-flow.md reference as part of the classroom generation phase. - No changes to user interaction flow or core SOP.","fileCount":11,"zipByteSize":23038},{"version":"0.3.6","createdAt":"2026-09-18T04:37:19.168Z","changelog":"Version 0.3.6 - Updated classroom generation guidance in references/generate-flow.md. - Removed outdated skill-card.md file. - No changes to workflow or phase structure; core guidance remains the same. - Minor documentation cleanup.","fileCount":11,"zipByteSize":22739},{"version":"0.3.5","createdAt":"2026-09-03T10:15:14.611Z","changelog":"- Removed the unused or duplicate skill-card.md file for clarity and maintenance. - Updated references/provider-keys.md guidance (details not shown). - No changes to core logic or user-facing instructions in SKILL.md. - Total file count reduced by one for a cleaner repository.","fileCount":11,"zipByteSize":22568},{"version":"0.3.4","createdAt":"2026-09-02T08:31:04.042Z","changelog":"## openmaic v0.3.4 Changelog - Skill documentation improved: updated the extend/build (二次开发) instructions in references/extend.md for greater clarity. - Removed the skill-card.md file to streamline documentation. No runtime changes to user experience or pipeline logic. - All setup and response rules remain consistent with previous versions.","fileCount":11,"zipByteSize":22409}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s177bd906czjkr4f79ahq4wj8183m0w4:openmaic","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s177bd906czjkr4f79ahq4wj8183m0w4:openmaic` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/wyuc/openmaic before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-09T14:59:27.559Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-wyuc-openmaic/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-09T03:25:13.136Z","emptyReason":null},"readme":"Skill: OpenMAIC\n\nOwner: wyuc\n\nSummary: OpenMAIC assistant for setting up, generating, and extending OpenMAIC. Use when the user wants to use OpenMAIC, generate a multi-agent interactive classroom, or build on / extend / customize OpenMAIC and its @openmaic/* SDK (secondary development, 二开) — covers Live Demo or local setup, startup modes, provider keys, classroom generation, and secondary development (forking, providers/storage/themes, routes, or the renderer/editor).\n\nTags: latest:0.3.11\n\nVersion history:\n\nv0.3.11 | 2026-10-04T14:41:35.081Z | auto\n\n- Updated the classroom generation phase to clarify retry behavior on failed jobs (now references retrying failed jobs per the generate flow).\n- Minor clarification in the classroom generation step about submitting supported fields and the handling of optional features.\n- Removed obsolete documentation file skill-card.md.\n- No changes to core setup or workflow phases.\n\nv0.3.10 | 2026-10-02T12:13:36.908Z | auto\n\nopenmaic 0.3.10\n\n- Improved provider configuration instructions—users now update `openmaic.yml`, `.env.local`, or web app settings, instead of legacy config files.\n- Clarified that all provider/model selection is controlled solely by OpenMAIC server-side config; request-time overrides are not supported.\n- Enhanced classroom generation: only permitted fields (`requirement`, `materialIds`) are submitted, matching server config for optional features.\n- Updated confirmation requirements for file uploads; confirmation is required before reading and uploading any local files.\n- Response instructions and phase explanations have been refined for clarity, especially regarding Live Demo and extension/SDK workflows.\n- Removed outdated references and files, including skill-card.md, reflecting the revised setup flow.\n\nv0.3.9 | 2026-09-29T09:52:00.439Z | auto\n\nOpenMAIC v0.3.9\n\n- Updated content in references/extend-cookbook.md and references/startup-modes.md for improved clarity and guidance.\n- Removed the unused skill-card.md file.\n- No structural or workflow changes—functionality and rules remain as previously documented.\n\nv0.3.8 | 2026-09-28T03:55:20.573Z | auto\n\nopenmaic 0.3.8\n\n- Updated documentation: references/extend-cookbook.md revised for improved secondary development guidance.\n- Removed skill-card.md to streamline documentation.\n- No changes to core functionality—this is a docs/maintenance release.\n\nv0.3.7 | 2026-09-23T05:26:50.410Z | auto\n\n- Removed the unused skill-card.md file.\n- Updated generate-flow.md reference as part of the classroom generation phase.\n- No changes to user interaction flow or core SOP.\n\nv0.3.6 | 2026-09-18T04:37:19.168Z | auto\n\nVersion 0.3.6\n\n- Updated classroom generation guidance in references/generate-flow.md.\n- Removed outdated skill-card.md file.\n- No changes to workflow or phase structure; core guidance remains the same.\n- Minor documentation cleanup.\n\nv0.3.5 | 2026-09-03T10:15:14.611Z | auto\n\n- Removed the unused or duplicate skill-card.md file for clarity and maintenance.\n- Updated references/provider-keys.md guidance (details not shown).\n- No changes to core logic or user-facing instructions in SKILL.md.\n- Total file count reduced by one for a cleaner repository.\n\nv0.3.4 | 2026-09-02T08:31:04.042Z | auto\n\n## openmaic v0.3.4 Changelog\n\n- Skill documentation improved: updated the extend/build (二次开发) instructions in references/extend.md for greater clarity.\n- Removed the skill-card.md file to streamline documentation. No runtime changes to user experience or pipeline logic.\n- All setup and response rules remain consistent with previous versions.\n\nv0.3.3 | 2026-08-16T08:12:33.900Z | auto\n\n- Added support for secondary development (二次开发) guidance: users can now get instructions for extending or building on OpenMAIC or its SDK, with new dedicated references.\n- Phase 0 now includes an \"Extend/build on OpenMAIC\" branch, bypassing Live Demo shortcuts even if an access code is present.\n- Updated skill description and SOP to clarify SDK/extend use cases and distinguish Live Demo, local, and extend modes.\n- Added new reference files for secondary development: extend.md, extend-cookbook.md, and extend-sdk.md.\n- Removed outdated skill-card.md file.\n- Improved and clarified setup flow, edge cases, and user guidance in SKILL.md.\n\nv0.3.2 | 2026-08-06T02:36:33.585Z | auto\n\n**Cloud Live Demo replaces \"hosted mode\"; setup docs and flows updated accordingly.**\n\n- Renamed all \"hosted mode\" references to \"Live Demo\" and aligned startup flow with the new official OpenMAIC cloud service (open.maic.chat).\n- Added new quickstart and access code instructions for Live Demo in config flow and user prompts.\n- Retired and removed stale hosted-mode and skill-card docs; added live-demo reference documentation.\n- Updated SOP to reflect Live Demo as the new default/recommended method, including details for access code management and UI navigation.\n- No changes to local setup or classroom generation instructions; local/server-side configuration steps remain the same.\n\nv0.3.1 | 2026-03-26T07:06:30.928Z | user\n\nclarify language field accepts only zh-CN | en-US; update stale enableTTS description to reflect actual server-side TTS capability\n\nv0.3.0 | 2026-03-20T14:34:38.361Z | auto\n\n- Major update: Introduces a full SOP-style, phase-based guided setup and usage flow for OpenMAIC, handling both hosted and local modes.\n- Adds new reference files for cloning, flow, startup modes, provider keys, and hosted mode to structure step-by-step guidance.\n- Now enforces explicit confirmation before all state-changing actions, progressing only one phase at a time.\n- Integrates support for optional skill config, defaults, and local/hosted mode decision logic.\n- Moves all credential handling to server-side configs and guides the user without requesting keys in chat or offering to write configs.\n- Refines response style: shorter steps, explicit options, and a clear summary after each completed action.\n\nv1.0.0 | 2026-03-20T14:30:16.600Z | auto\n\n- Major simplification: Replaced detailed step-by-step SOP with a concise, direct guide for generating AI-powered interactive classrooms.\n- Removed all in-depth setup, confirmation phases, and config instructions.\n- Skill now activates via natural language triggers (e.g., \"teach me\", \"generate classroom\", \"上课\").\n- Workflow: Collect topic requirements, generate a classroom, monitor progress, and share the classroom link.\n- Language can be specified (defaults to Chinese).\n- Basic error handling added: explain failures and offer to help refine user requests.\n\nv0.2.1 | 2026-03-16T03:46:08.690Z | user\n\nSecurity: access codes are now read from skill config file (~/.openclaw/openclaw.json) instead of being pasted into chat. Skill auto-detects stored access code and skips mode selection. Users are guided to edit the config file for credential setup.\n\nv0.2.0 | 2026-03-16T03:39:22.891Z | user\n\nAdd hosted mode: users with an access code from open.maic.chat can skip local setup and generate classrooms directly via the hosted API. Includes Phase 0 mode selection, Bearer token auth, independent quota (10/day), and error handling guidance.\n\nv0.1.0 | 2026-03-14T07:30:41.834Z | auto\n\nInitial release of the OpenMAIC skill, providing a guided, confirmation-based SOP for setup and classroom generation:\n\n- Guides users step-by-step through cloning or selecting the OpenMAIC repo, selecting a startup mode, and configuring provider API keys.\n- Enforces explicit confirmation before any state-changing actions.\n- Directs users to edit local config files themselves—never asks for API keys to be pasted into chat.\n- Includes clear instructions for starting OpenMAIC, verifying service health, and generating classrooms from requirements or PDFs.\n- Utilizes local OpenClaw config defaults if present, but always confirms actions with the user.\n- Outlines response and confirmation practices for a safe, user-involved setup flow.\n\nArchive index:\n\nArchive v0.3.11: 11 files, 29344 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7779b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (18835b), references/live-demo.md (3075b), references/provider-keys.md (8598b), references/startup-modes.md (1893b), skill-card.md (1877b), SKILL.md (6722b), _meta.json (128b)\n\nFile v0.3.11:SKILL.md\n\n---\nname: openmaic\ndescription: OpenMAIC assistant for setting up, generating, and extending OpenMAIC. Use when the user wants to use OpenMAIC, generate a multi-agent interactive classroom, or build on / extend / customize OpenMAIC and its @openmaic/* SDK (secondary development, 二开) — covers Live Demo or local setup, startup modes, provider keys, classroom generation, and secondary development (forking, providers/storage/themes, routes, or the renderer/editor).\nuser-invocable: true\nmetadata: { \"openclaw\": { \"emoji\": \"🏫\" } }\n---\n\n# OpenMAIC Skill\n\nUse this as a guided, confirmation-heavy SOP. Do not compress the whole setup into one reply and do not perform state-changing actions without explicit user confirmation.\n\n## Core Rules\n\n- Move one phase at a time.\n- Before any state-changing action, ask for confirmation.\n- If local state already exists, show what you found and ask whether to keep it.\n- Do not assume the OpenClaw agent's own model or API key will be reused by OpenMAIC.\n- OpenMAIC classroom generation uses OpenMAIC's server-side model configuration (`openmaic.yml` and the model settings in the web app).\n- This skill must not rely on any request-time model or provider overrides.\n- Only that server-side configuration may control provider selection and defaults.\n- Do not default to asking the user to paste API keys into chat.\n- Prefer guiding the user to edit local config files themselves.\n- Do not offer to write API keys into config files on the user's behalf.\n- Once setup is complete and the user clearly asks to generate a classroom, do not ask for a second confirmation before submitting the generation job.\n- Keep confirmations for local file reads such as reading a PDF from disk before uploading it.\n\n## Optional Skill Config\n\nIf present, read defaults from `~/.openclaw/openclaw.json` under:\n\n```jsonc\n{\n  \"skills\": {\n    \"entries\": {\n      \"openmaic\": {\n        \"enabled\": true,\n        \"config\": {\n          \"accessCode\": \"sk-xxx\",\n          \"repoDir\": \"/path/to/OpenMAIC\",\n          \"url\": \"http://localhost:3000\"\n        }\n      }\n    }\n  }\n}\n```\n\n- If `accessCode` is present, default to Live Demo mode and skip the mode-selection prompt — unless the user's intent is to extend/build on OpenMAIC (see the exception in Phase 0).\n- Use `repoDir` and `url` only as defaults for local mode.\n- Still confirm before acting.\n\n## SOP Phases\n\n### 0. Choose Mode\n\nFirst check skill config for `accessCode`. If present, announce that a stored access code was found and proceed directly to Live Demo mode (load [references/live-demo.md](references/live-demo.md), skip phases 1–4). Do not ask the user to paste the code again. **Exception:** if the user's stated intent is to extend / build on / customize OpenMAIC or consume the `@openmaic/*` SDK (二次开发 / 二开 / SDK), do not auto-shortcut — go to the extend branch below regardless of `accessCode`. A returning Live Demo user who now wants to do 二开 should be routed to extend, not silently sent back to Live Demo.\n\nIf no `accessCode` in config (or the extend exception above applies), ask the user how they want to use OpenMAIC:\n\n1. **Use the OpenMAIC Live Demo** (recommended for quick start) — The cloud edition: the version officially deployed and hosted by the OpenMAIC team at open.maic.chat. Requires an access code (starts with `sk-`). Get yours by signing in at https://open.maic.chat, clicking your account in the top-right corner, opening \"访问码设置\" (access code settings), and generating a code; then add it to `~/.openclaw/openclaw.json` under `skills.entries.openmaic.config.accessCode`. No local setup needed.\n2. **Run locally** — Clone the repo, configure provider keys, and run on your machine.\n3. **Extend or build on OpenMAIC (二次开发)** — Fork the repo and customize the product, or consume the `@openmaic/*` SDK to build something new.\n\nIf the user chooses Live Demo mode, load [references/live-demo.md](references/live-demo.md) and skip phases 1–4.\nIf the user chooses local mode, proceed to phase 1 as usual.\nIf the user chooses to extend/build on OpenMAIC, load [references/extend.md](references/extend.md) and skip the setup/generation phases.\n\n### 1. Clone Or Reuse Existing Repo\n\nLoad [references/clone.md](references/clone.md).\n\nUse this when the user has not installed OpenMAIC yet or when you need to confirm which local checkout to use.\n\n### 2. Choose Startup Mode\n\nLoad [references/startup-modes.md](references/startup-modes.md).\n\nUse this after the repo location is confirmed. Present the available startup modes, recommend one, and wait for the user's choice.\n\n### 3. Configure Provider Keys\n\nLoad [references/provider-keys.md](references/provider-keys.md).\n\nUse this before starting classroom generation. Recommend a provider path and tell the user exactly what to edit themselves (`openmaic.yml` and `.env.local`, or the model settings in the web app). If generation later fails due to provider/model/auth issues, return to this phase and direct the user to update the same configuration.\n\nAfter the core LLM key is configured, ask the user if they want to enable optional features (web search, image generation, video generation, TTS). Each is a slot with its own provider — see the \"Optional Features\" section in provider-keys.md.\n\n### 4. Start And Verify OpenMAIC\n\nAfter the user has chosen a startup mode and configured keys, start OpenMAIC using the chosen method, then verify the service with `GET {url}/api/health`.\n\n### 5. Generate A Classroom\n\nLoad [references/generate-flow.md](references/generate-flow.md).\n\nUse this only after the service is healthy. Confirm before reading local files to upload. If the user has already clearly asked to generate, do not ask for a second confirmation before submitting the generation job, and then follow the polling loop until it succeeds or fails (a failed job may be retried; see the generate flow). Only send the supported fields (`requirement`, `materialIds`) for generation requests; optional features follow the server's provider config. Uploads and the submission must resolve to the same owner: in anonymous-cookie mode reuse the same cookie jar on every request. For long-running jobs, prefer sparse polling and tell the user to check back later if the turn ends before completion.\n\n## Response Style\n\n- Keep each step short and explicit.\n- Prefer 2-3 concrete options when the user must choose.\n- Always include the recommended option first and explain why in one sentence.\n- After a step completes, say what changed and what the next confirmation is for.\n- When returning a classroom link, place the raw absolute URL on its own line with no bold, markdown link syntax, code formatting, or tables.\n\nFile v0.3.11:_meta.json\n\n{\n  \"ownerId\": \"kn7cjfn5dkygrges9scanxv97d82x3t1\",\n  \"slug\": \"openmaic\",\n  \"version\": \"0.3.11\",\n  \"publishedAt\": 1791124895081\n}\n\nFile v0.3.11:references/clone.md\n\n# Clone Or Reuse Existing Repo\n\n## Goal\n\nEstablish which OpenMAIC checkout will be used for setup and runtime actions.\n\n## Procedure\n\n1. Check whether OpenMAIC already exists locally.\n2. If a checkout exists, show the path and ask whether to reuse it.\n3. If no checkout exists, propose cloning the repo and ask for confirmation.\n4. After clone, confirm dependency installation separately.\n\n## Recommended Path\n\n- Recommended: reuse an existing checkout if it is already on the target branch.\n- Otherwise: clone a fresh checkout from GitHub, then install dependencies.\n\n## Commands\n\nClone:\n\n```bash\ngit clone https://github.com/THU-MAIC/OpenMAIC.git\ncd OpenMAIC\n```\n\nInstall dependencies:\n\n```bash\npnpm install\n```\n\n## Confirmation Requirements\n\n- Ask before `git clone`.\n- Ask before `pnpm install`.\n- If the repo is dirty, tell the user and ask whether to continue with that checkout.\n\nFile v0.3.11:references/extend-cookbook.md\n\n# Extend The OpenMAIC Product (Cookbook)\n\n## Scope\n\nYou are working **inside a fork of the OpenMAIC product** (the Next.js app at the repo root), customizing it in place. If instead you want to consume `@openmaic/*` in a separate app, use [extend-sdk.md](extend-sdk.md) instead.\n\nEntry points below are given as **file + symbol name** (not line numbers — they drift). Read the file, then jump to the symbol. The `@/*` path alias is anchored at the repo root.\n\n## Task 1 — Swap Or Add An AI Provider\n\n**Goal:** route generation to a different provider, or register a brand-new one.\n\nTwo distinct cases:\n\n- **Use an already-supported provider** (it's in the union below): no source change. Declare it in `openmaic.yml` with its preset and assign it to a slot, key in `.env.local` as `${VAR}` — follow [provider-keys.md](provider-keys.md). Chat slots always name `<provider id>:<model id>`.\n- **Register a NEW provider** (source change):\n  1. Add the id to the `BuiltInProviderId` union in `lib/types/provider.ts`.\n  2. Register its config + models in the `PROVIDERS` registry in `lib/ai/providers.ts`.\n  3. The registry entry becomes a preset automatically (`lib/config/provider-presets.ts`; the preset id is the registry id unless `lib/config/preset-ids.ts` overrides it), so `openmaic.yml` can declare it with `preset: your-id` and the model settings offer it. No env wiring is needed.\n  4. Optional, legacy only: to also accept `<PREFIX>_API_KEY` / `_BASE_URL` / `_MODELS` from the environment without `openmaic.yml`, add a `PREFIX: 'your-id'` entry to `LLM_ENV_MAP` in `lib/server/provider-config.ts`. That path is deprecated.\n  5. A new token plan (one key, several capabilities) is one entry in `lib/config/token-plan-presets.ts`; it becomes a preset whose recommended models fill the slots it covers in the first-run setup.\n\n**Gotcha:** OpenMAIC has **no hardcoded model fallback**. If no model is assigned to the `llm` slot (or the more specific slot a call uses), generation fails with `No model is configured for <slot>` rather than picking a vendor — always assign one.\n\n## Task 2 — Server-Side Persistence (PostgreSQL / S3)\n\n**Goal:** understand where documents, runtime state and assets live, and point them at your database.\n\nAccurate topology (there is **no local-file backend**, and no browser-storage mode):\n\n- **Always server-backed:** documents, runtime state and assets are stored on the server. The server **refuses to start without `DATABASE_URL`**; locally, `pnpm db:up` starts a separate development PostgreSQL (its own Compose project and volume) on `127.0.0.1` and `.env.example` carries the matching `DATABASE_URL`.\n- **Client side:** `lib/persistence/bootstrap.ts` configures the browser's HTTP-backed `HttpRuntimeStore` / `HttpDocumentStore` / `HttpAssetStore`, which call `/api/persistence`. Those calls carry no credential of their own: the server attributes them to the owner the owner identity seam (`lib/server/identity/`) resolves, and the runtime learner key is that owner id. Only device-local state (settings, playback position, local media cache) stays in the browser (`lib/device-storage/`).\n- **Server side:** the `/api/persistence` catch-all (`app/api/persistence/[...path]/route.ts`) persists **documents + runtime to PostgreSQL**, and **asset bytes to PostgreSQL or S3**. The byte-layer selection lives in `lib/persistence/asset-byte-store.ts` (`configuredS3Bucket` / `lazyAssetByteStore`) and is strictly three-way: **unset/empty** `ASSET_S3_BUCKET` ⇒ `PgAssetByteStore`; a **valid** bucket ⇒ S3; an **invalid** bucket name ⇒ asset operations **fail** — validation throws, there is no fallback to PG. (The failure isn't cached: the next asset request retries, and only asset traffic is affected — document/runtime requests keep working.)\n- The backends themselves come from `@openmaic/storage` subpaths (`@openmaic/storage/document/pg`, `@openmaic/storage/runtime/pg`, `@openmaic/storage/asset/pg-bytes`, `@openmaic/storage/asset/s3-bytes`) — see the storage table in [extend-sdk.md](extend-sdk.md).\n\n**Gotcha:** S3 additionally needs `@aws-sdk/client-s3` (optional peer of `@openmaic/storage`) installed in the app, and PG needs a reachable Postgres + the package's schema-ensure step. The removed `NEXT_PUBLIC_PERSISTENCE` build switch is ignored.\n\n## Task 3 — Branding / UI / Theme\n\n**Goal:** change title, fonts, color tokens, or preset themes.\n\nEntry points:\n\n- Title + fonts: `app/layout.tsx` (the title metadata; fonts include `@openmaic/renderer/fonts.css`).\n- Design tokens: `app/globals.css` — Tailwind v4 (`@import 'tailwindcss'`, the `@source` directives that scope the renderer's classes, and the `@theme inline { … }` block for custom tokens).\n- Preset themes: the `PRESET_THEMES` array in `configs/theme.ts`.\n\n**Gotcha:** This is Tailwind **v4** — config lives in CSS via `@theme`/`@source`, **not** in `tailwind.config.js`. The renderer's class names are discovered through the `@source` scans of its `dist`; keep token names consistent across `@theme` and the renderer or styles silently drop.\n\n## Task 4 — Add A Page Or API Route\n\n**Goal:** ship a new screen or a new server endpoint.\n\nEntry points:\n\n- Page (App Router): `app/<segment>/page.tsx`.\n- API route: `app/api/<segment>/route.ts`.\n- Reuse the shared server helpers instead of reimplementing: `lib/server/api-response.ts`, `lib/server/llm-error-response.ts`, `lib/server/ssrf-guard.ts`, `lib/server/proxy-fetch.ts`.\n- Import app code via the `@/*` alias (e.g. `import { … } from '@/lib/…'`).\n\n**Gotcha:** Any route that fetches a user-supplied URL must go through `lib/server/ssrf-guard.ts` and `lib/server/proxy-fetch.ts` — do not call `fetch()` directly with attacker-controlled hosts. LLM error responses should go through `llm-error-response.ts` to stay consistent with the rest of the API.\n\n## Task 5 — Embed The Renderer Or Editor\n\n**Goal:** drop a slide canvas (read-only) or the full editable slide surface into a component.\n\nEntry points (real usage examples to copy):\n\n- Render-only `SlideCanvas` from `@openmaic/renderer`: see `components/slide-renderer/SlideThumbnail.tsx`.\n- Editable surface `EditableSlideCanvasWithUI` from `@openmaic/editor/ui`: see `components/edit/surfaces/slide/RendererEditorCanvas.tsx`.\n- Types for slide data: `Slide`, `PPTElement` from `@openmaic/dsl`.\n\n**Gotcha (CSS is mandatory):** The renderer renders unstyled/broken without its fonts and Tailwind classes. In the consuming layout/globals: `@import '@openmaic/renderer/fonts.css';` (or the JS import form), and ensure the renderer's classes are in Tailwind v4's content scan (an `@source` directive pointing at the renderer's `dist`). Note `fonts.css` is **generated** (regen via the renderer's `genfonts` script) and self-hosts CJK faces by fetching woff2 on demand from `https://file.maic.chat/fonts/<name>.woff2` — relevant for offline/custom-font operation. The editor transitively requires the renderer's full peer stack (Tailwind v4, `motion`, optionally `echarts`/`shiki`) — not just `react`/`react-dom`.\n\n## After Your Edits — Run And Verify\n\nThis product still runs like the stock app; don't reinvent the startup steps:\n\n- Dev server / startup mode → [startup-modes.md](startup-modes.md).\n- Provider keys the running server needs → [provider-keys.md](provider-keys.md).\n- Verify with `GET {url}/api/health` (Phase 4), then confirm UI/route changes in the browser.\n\nRebuild sequence when you touch `packages/@openmaic/*` source: consumers resolve to the built `dist/`, not `src/`, so rebuild the changed package (dependency order: `dsl → generation → storage → importer → renderer → editor`). `pnpm install`'s postinstall already does this in order; for a single package use its `pnpm run build`.\n\nFile v0.3.11:references/extend-sdk.md\n\n# Consume The @openmaic/* SDK (Build A New App)\n\n## Scope\n\nYou are building a **separate app** that consumes `@openmaic/*` packages via `npm install` — not working inside the OpenMAIC monorepo. If you are customizing the OpenMAIC product itself, use [extend-cookbook.md](extend-cookbook.md) instead.\n\nThe SDK is early-stage (`0.x`). Versions below are current as of this writing — confirm against the registry when you install.\n\n## Package Quick Reference\n\n| Package | Version | Purpose | Key Exports | Peer Deps |\n|---|---|---|---|---|\n| `@openmaic/dsl` | 0.8.0 | The slide **contract** — types + JSON schema. Zero runtime deps. | `Slide`, `PPTElement`, `./schema/*` | none |\n| `@openmaic/renderer` | 0.1.0 | Render slides to DOM (read-only). | `SlideCanvas`, `./snapshot`→`slideToPng` | react ≥18, react-dom ≥18, motion ≥11, tailwindcss ≥4; **optional:** echarts ≥5, shiki ≥1 |\n| `@openmaic/editor` | 0.0.2 | Editable slide surface + prosemirror editor. | `EditableSlideCanvasWithUI` (from `./ui`) | react ≥18, react-dom ≥18 **+ renderer's full peer stack transitively** |\n| `@openmaic/generation` | 0.3.0 | LLM-driven scene/lesson generation. | `generateSceneContent` | none |\n| `@openmaic/storage` | 0.2.5 | Document / Runtime / Asset / KV stores (Browser · HTTP · PG · S3). | see table below | **optional:** `@aws-sdk/client-s3` |\n| `@openmaic/importer` | 0.1.2 | Import PPTX / PDF into the DSL. | `importPptx` | none |\n\n> `editor` is `0.0.2`. Its own `peerDependencies` list only `react`/`react-dom`, but it `dependencies` on `@openmaic/renderer`, so you must also satisfy the renderer's peers (tailwindcss v4, motion, and optionally echarts/shiki) or it breaks at render time.\n\n## Minimal Starter — Render A Slide\n\n1. Install peers and the package (pin exact versions for `0.x`):\n   ```bash\n   npm i @openmaic/dsl@0.8.0 @openmaic/renderer@0.1.0 \\\n         react@^18 react-dom@^18 motion@^11 tailwindcss@^4\n   # only if you render charts / code-highlighted blocks:\n   npm i echarts@^5 shiki@^1\n   ```\n2. Mandatory CSS — without it the canvas renders unstyled:\n   ```css\n   @import 'tailwindcss';\n   @import '@openmaic/renderer/fonts.css';\n   ```\n   ...and ensure Tailwind v4 scans the renderer's classes, e.g. `@source '../node_modules/@openmaic/renderer/dist';` in your globals. (`fonts.css` is generated and fetches CJK woff2 on demand from `https://file.maic.chat/fonts/<name>.woff2` — relevant for offline/custom-font needs.)\n3. Render:\n   ```tsx\n   import { SlideCanvas } from '@openmaic/renderer';\n   import type { Slide } from '@openmaic/dsl';\n   ```\n   For a working example of props/usage, read `components/slide-renderer/SlideThumbnail.tsx` in the OpenMAIC repo.\n\n## Precise Import Paths\n\n| Want | Import |\n|---|---|\n| Slide / element types | `@openmaic/dsl` (`Slide`, `PPTElement`) |\n| JSON schema (validation) | `@openmaic/dsl/schema/*` |\n| Render a slide | `@openmaic/renderer` (`SlideCanvas`) |\n| Snapshot a slide → PNG | `@openmaic/renderer/snapshot` (`slideToPng`) |\n| Editable slide surface | `@openmaic/editor/ui` (`EditableSlideCanvasWithUI`) |\n| Generate lesson content | `@openmaic/generation` (`generateSceneContent`) |\n| Import a PPTX | `@openmaic/importer` (`importPptx`) |\n| Storage backends | see table below |\n\n## Storage Backends — Exact Subpaths\n\nThere is **no** bare `@openmaic/storage/document`, `/runtime`, or `/asset` subpath — always use the **backend-suffixed** form. The main barrel (`@openmaic/storage`) is asymmetric:\n\n| Domain | Browser | HTTP | PG | S3 |\n|---|---|---|---|---|\n| **Document** | barrel `.` | `./document/http` | `./document/pg` | — |\n| **Runtime** | barrel `.` | `./runtime/http` ⚠️ | `./runtime/pg` ⚠️ | — |\n| **Asset** | barrel `.` | `./asset/http` | `./asset/pg`, `./asset/pg-bytes` | `./asset/s3-bytes` |\n| **KV** | barrel `.` | `./kv/http` | — | — |\n| Server helpers | `./server`, `./server/reference` | | | |\n\n⚠️ **Runtime asymmetry (the main gotcha):** `HttpRuntimeStore` and `PgRuntimeStore` are **only** reachable via `@openmaic/storage/runtime/http` and `@openmaic/storage/runtime/pg` — they are **not** in the main barrel. `BrowserRuntimeStore` is. Document/Asset/KV backends are all in the barrel. Importing `PgRuntimeStore` from `@openmaic/storage` will fail with \"not exported\".\n\nPG backends ship an `ensureSchema` (plus a `*_PG_SCHEMA` constant) you must run once. S3 needs `@aws-sdk/client-s3` installed.\n\n## Version Pinning\n\nAll six packages are `0.x`. Pin **exact** versions (e.g. `\"@openmaic/renderer\": \"0.1.0\"`), not `^` ranges — `0.x` semver treats minor bumps as breaking, and these packages are still moving fast.\n\n## If You Need To Change The SDK Itself\n\nConsuming via `npm install` means you treat the SDK as a black box. To modify SDK behavior, the path is heavier:\n\n1. Fork `THU-MAIC/OpenMAIC`, edit the package source under `packages/@openmaic/*`, rebuild its `dist/`.\n2. Produce an installable artifact for **just that subpackage** and consume it in your app — e.g. `npm pack` the modified package and `npm install` the tarball, or publish it under a private/different package name and depend on that. ⚠️ A plain Git dependency or npm `overrides` / `resolutions` pointing at the repo **won't work**: a Git dependency resolves to the repository's root package, not `packages/@openmaic/<name>`.\n3. Keep your fork's diff small and track upstream — the SDK is actively versioned.\n\nFile v0.3.11:references/extend.md\n\n# Extend Or Build On OpenMAIC (二次开发)\n\n## Charter\n\nSecondary development is a confirmation-heavy, **read-before-modify** guidance flow — not a generation flow. Help the user understand the existing code first, then make targeted changes. Default to **not** editing source under `packages/@openmaic/*`; consume those packages as-is. (Modifying the SDK itself is a different, heavier path — see the last section of [extend-sdk.md](extend-sdk.md).)\n\nThis reference takes priority over the `accessCode` auto-shortcut in Phase 0: if the user's intent is to extend / build on / customize OpenMAIC or consume the `@openmaic/*` SDK, enter this flow **even when a stored `accessCode` exists**. A returning Live Demo user who now wants to do 二开 should be routed here, not silently sent back to Live Demo.\n\n## Secondary-Development Rules\n\n1. **Read before edit.** Before changing any file, read it (and the symbols it imports) so the edit matches surrounding conventions. Do not paste large code blocks into chat — point the user at `file:line` entry points and let them read.\n2. **Toolchain is hard-required.** `pnpm@10.28.0` (root `packageManager`), Node `>=22.19.0` (`.nvmrc` pins `22`). Mismatched pnpm will fail install.\n3. **Forking and disabling CI are conditional, not defaults.** Decide per the user's intent — see Development Environment below — instead of reflexively forking every user.\n\n## Development Environment (Same As Local Deployment)\n\n二开的开发环境本质上就是 OpenMAIC **本地部署环境**——同一套工具链、同一个仓库、同一次 `pnpm install`、同一套 provider key 和启动方式。所以**环境搭建不要在这里另搞一套**：走标准本地部署流程拿到一个能跑的实例，二开只在其上加几个增量。\n\n**在哪里拿代码（按需选，不强制 fork）：**\n\n- **自用 / 不需要远程**：直接 `git clone` 上游 `THU-MAIC/OpenMAIC`，本地改、本地跑。最简单——不 fork、不管 CI。你对上游无写权限，不可能误推；建议本地 `git commit` 到一条分支做版本回滚。\n- **需要远程**（备份 / 多机同步 / 协作 / 从 GitHub 部署 / 回馈上游）：fork → clone 你的 fork → 推到 fork。fork 的唯一意义是\"拥有一个能 push 的远程\"。\n\n**安装与启动** → 复用现有本地部署 reference，不要重写流程：[clone.md](clone.md)（clone + `pnpm install`，后者会在 postinstall 构建全部 `@openmaic/*` 包并同步 vendor 包）、[startup-modes.md](startup-modes.md)（启动方式）、[provider-keys.md](provider-keys.md)（provider key）。\n\n**禁用 publish CI —— 注意\"触发 workflow\"≠\"跑 publish job\"：** fork 自带 `.github/workflows/publish-packages.yml` 和 `publish-openmaic-skill.yml`，要分两层看：\n\n- **触发层（workflow 什么时候跑）**：`publish-packages.yml` 只在 **push 到 main 且改了 `packages/@openmaic/*/package.json`** 时触发（PR 根本不触发这个 workflow）；`publish-openmaic-skill.yml` 在 **PR 或 push 到 main 且动了 `skills/openmaic/**`** 时触发。\n- **job 层（哪些 job 会跑）**：`publish-openmaic-skill.yml` 在 **PR 上只跑** bash-3 兼容性 + preview（dry-run）job——这两个不需要 token；**带 `CLAWHUB_TOKEN` 的 publish job 只在 push（或 main 上的手动非 dry-run dispatch）时运行**。`publish-packages.yml` 的带 `NPM_TOKEN` 的 publish job 同样只在 push 时跑。\n\n所以 fork 里的 token 红叉**只来自命中触发条件的 push**，PR 不会产生；普通 feature 分支推送不匹配触发条件则整个 workflow 都不跑。是否禁用取决于你的 fork 工作流：会往 main 推命中触发的改动就禁用（或去掉触发），否则不用管；纯本地自用、从不 push 同样无需处理。误发版本身已被 environment + token 闸门挡死，不用担心。\n\n**改完代码后运行 / 验证：** 启动方式同 [startup-modes.md](startup-modes.md)，key 同 [provider-keys.md](provider-keys.md)，用 `GET {url}/api/health` 验证，UI / 路由改动在浏览器确认。若你改动了 `packages/@openmaic/*` 的**源码**，要先重建对应包的 `dist/`（消费方解析的是 `dist/` 不是 `src/`；依赖顺序 `dsl → generation → storage → importer → renderer → editor`，`pnpm install` 的 postinstall 已按此顺序构建，单包可用各自 `pnpm run build`）。\n\n## Route To The Right Sub-Reference\n\nAsk the user which of these they want. When unsure, offer 2–3 examples (below) and let them self-identify before loading anything.\n\n- **Customize the OpenMAIC product itself** (change a feature / UI / swap in your own provider, storage, or theme / add a page or API route / embed the renderer or editor inside the product) → Load [extend-cookbook.md](extend-cookbook.md).\n- **Use the `@openmaic/*` SDK to build a new, standalone app** (outside this repo — `npm install @openmaic/renderer` into your own project, not a monorepo workspace) → Load [extend-sdk.md](extend-sdk.md).\n\nTypical examples to help the user pick:\n\n- \"I want OpenMAIC to call my company's LLM / write to my S3 bucket / show my branding\" → **cookbook** (you are modifying the product).\n- \"I want to render OpenMAIC slides, or embed the slide editor, inside my own separate app\" → **SDK** (you are consuming packages, not editing the product).\n- \"I want to add a feature to OpenMAIC and ship it in the product\" → **cookbook** (modifying the product).\n\n## General Gotchas\n\nEach is tagged with where it bites: **[product/fork]** (working inside the OpenMAIC repo), **[SDK]** (consuming packages in a separate app), or **[both]**.\n\n- **[product/fork] `dist/` is gitignored in a source checkout.** In a fork/monorepo, each `@openmaic/*` package ships source only; its `dist/` is produced by `postinstall` (builds `dsl → generation → storage → importer → renderer → editor` in dependency order). If you `git clean -fdx` or nuke `node_modules`, re-run `pnpm install` or the packages won't resolve. (Published npm packages — the [SDK] path — ship built `dist/`, so this does not apply there.)\n- **[product/fork] The vendor bundle is asserted before build.** `pnpm build` runs `node scripts/assert-vendor-maic-importer.mjs && next build`. That guard `stat()`s `public/vendor/maic-importer/index.js`; if missing/empty it exits 1 with an actionable message. `postinstall`'s `sync-maic-importer.mjs` step populates it — re-run `pnpm run sync:maic-importer` if you cleared it.\n- **[product/fork] `workspace:*` are symlinks.** Inside the monorepo, `@openmaic/*` resolve to live `packages/@openmaic/*` source via pnpm workspace links. Editing a package's `src/` is picked up on its next build — but consumers see the built `dist/`, not `src/`, so rebuild the package after source changes.\n- **[both] The renderer hard-depends on Tailwind v4.** `@openmaic/renderer` has `tailwindcss: \">=4\"` as a peer. Tailwind v4 uses `@theme`/`@source` CSS directives, not a JS config — see [extend-cookbook.md](extend-cookbook.md) (branding) before touching styles.\n- **[product/fork] `@/*` path alias** is anchored at the repo root (`tsconfig.json` → `\"@/*\": [\"./*\"]`), not inside `packages/`.\n\nFile v0.3.11:references/generate-flow.md\n\n# Generate Flow\n\n## Preconditions\n\n- Repo path is confirmed\n- Startup mode has been chosen\n- OpenMAIC is healthy at the selected `url`\n- Provider keys are configured\n\n> **Live Demo mode**: If using the OpenMAIC Live Demo (open.maic.chat), all\n> preconditions (repo, startup, provider keys) are already satisfied.\n> Include `Authorization: Bearer <access-code>` header on all requests below.\n> See [live-demo.md](live-demo.md) for details.\n\n> **Self-hosted with `ACCESS_CODE`**: a self-hosted server gated by\n> `ACCESS_CODE` does not accept `Authorization: Bearer`; it answers `401` to\n> every API request without its `openmaic_access` cookie (except\n> `/api/health`). Verify the code once, then reuse the same cookie jar on every\n> request below (it also carries the owner cookie, see the next section):\n>\n> ```bash\n> curl -c cookies.txt -b cookies.txt -X POST {url}/api/access-code/verify \\\n>   -H 'Content-Type: application/json' -d '{\"code\":\"<ACCESS_CODE>\"}'\n> ```\n>\n> The cookie lasts 7 days. It is `Secure` in production, so over plain HTTP\n> (other than localhost) the server needs `COOKIE_SECURE=0`, or the client\n> never sends it back.\n\n## Request Contract\n\n`POST {url}/api/generate-classroom` accepts exactly two fields:\n\n- `requirement` (string, required) — what the classroom should teach. The course language follows the requirement (and any uploaded material); there is no `language` field.\n- `materialIds` (string array, optional) — up to 5 ids returned by `POST {url}/api/materials`, used as source documents in the order given.\n\nNothing else is a request field. Web search, image generation, video generation and TTS narration are attempted automatically whenever their slot (`webSearch`, `image`, `video`, `tts`) resolves to a provider in the server's model configuration for the caller's workspace; they cannot be switched on or off per request, and requests never carry provider choices or API keys. Other fields are ignored, except `pdfContent`, which is rejected with `400 INVALID_REQUEST` (upload the document instead, see below).\n\nA job is a server-side generation run, the same pipeline the web app's classic generation uses: the server generates the outline, confirms it itself, and then generates course-specific agents, the scenes in order, their narration, and the images and videos the outline asks for. The job id is the run id.\n\nDo not rely on request-time model or provider override parameters. To change what a generation job can do, change the slots in `openmaic.yml` or in the model settings (a slot set to `null` is off).\n\n## Keep One Owner Across Requests\n\nUploaded materials belong to the owner the server resolves for the upload request, and `materialIds` only resolve for that same owner.\n\n- `GET {url}/api/generate-classroom/capabilities` needs no owner.\n- If the server resolves a fixed owner — a shared team owner (`PERSISTENCE_SHARED_OWNER_ID` together with `ACCESS_CODE`), single-user mode (`OWNER_SINGLE_USER=true`), or a host that resolves the owner from a credential you send on every request — every request is the same owner automatically.\n- Otherwise the server identifies callers by an anonymous owner cookie that it sets on the first owner-scoped response (including error responses). Reuse one cookie jar on every request of the flow — uploads, the submission, polls and deletions — for example `curl -c cookies.txt -b cookies.txt` on all calls. An upload made without the cookie belongs to a different owner, and its id is unavailable to the submission.\n\nThe job and the classroom it produces belong to the owner that submitted it:\n\n- A job can only be polled by that owner. A poll without the submission's cookie (or credential) is a different owner, and gets the same `404` as an unknown job.\n- The finished classroom is saved in that owner's course library. With a fixed owner it is editable wherever that owner signs in. With an anonymous owner cookie, the classroom opens read-only for anyone who has its URL but cannot be edited from a user's browser, because the browser is a different anonymous owner. If the user wants to edit classrooms generated through the API, the server needs a fixed owner or host authentication.\n\n## Optional: Check Capabilities\n\nTo tell the user in advance what the job is configured to attempt, or which files it can generate from, query:\n\n```text\nGET {url}/api/generate-classroom/capabilities\n```\n\n```json\n{\n  \"success\": true,\n  \"capabilities\": {\n    \"webSearch\": true,\n    \"imageGeneration\": false,\n    \"videoGeneration\": false,\n    \"tts\": true\n  },\n  \"materials\": {\n    \"formats\": [{ \"id\": \"pdf\", \"mime\": \"application/pdf\", \"extensions\": [\".pdf\"] }],\n    \"maxCount\": 5,\n    \"maxTotalBytes\": 157286400,\n    \"maxDocumentBytes\": 52428800,\n    \"maxMediaBytes\": 52428800\n  }\n}\n```\n\n`capabilities` says which optional features the server has a provider configured for; nothing needs to be sent back. A search that fails continues without its context. An image or video that fails does not fail the job: the classroom completes with a placeholder there, and `result.warning` counts the failures. Narration is part of each scene: a narration provider failure the server's retries cannot overcome fails the job like any other step (see the polling loop for Retry), while a clip the server could not store (the owner's asset storage is full) is left silent and counted in `result.warning`.\n\n`materials.formats` lists the upload types this server can extract with its current configuration (plain text, Markdown and PDF always; Office documents, images, audio and video only when a matching extraction service or local media pipeline is configured). Classroom generation uses the extracted text of each material, and the images the extraction finds in documents are stored with the classroom and can be placed on its slides. With the local media pipeline (ffmpeg) but no server ASR provider, video is listed and audio is not: a video without an audio track extracts, but contributes almost nothing (its text is just \"No audio track\"), and a video with an audio track fails when the job runs, because its speech cannot be transcribed. `maxCount` and `maxTotalBytes` bound one request's `materialIds`; the byte limits apply per file (`maxMediaBytes` for audio/video, `maxDocumentBytes` for everything else). `POST /api/materials` may accept more types than are listed here, but a submission with a material of an unlisted type is refused.\n\n## Requirement-Only Generation\n\nIf the user has already clearly asked to generate the classroom and the preconditions are satisfied, submit the generation job immediately. Do not ask for a second confirmation just before calling `/api/generate-classroom`.\n\n```text\nPOST {url}/api/generate-classroom\n```\n\n```json\n{\n  \"requirement\": \"Create an introductory classroom on quantum mechanics for high school students\"\n}\n```\n\nTreat the `POST` response as job submission only. Expect fields such as:\n\n```json\n{\n  \"success\": true,\n  \"jobId\": \"run-Q2xhc3Nyb29tSm9i\",\n  \"runId\": \"run-Q2xhc3Nyb29tSm9i\",\n  \"status\": \"queued\",\n  \"step\": \"queued\",\n  \"pollUrl\": \"http://localhost:3000/api/generate-classroom/run-Q2xhc3Nyb29tSm9i\",\n  \"pollIntervalMs\": 5000\n}\n```\n\nThe submission is refused before any job exists when:\n\n- a model the job needs (the outline, the actions, or scene content for at least one scene type) is not configured or its slot is turned off (`400 MISSING_MODEL`), its provider has no API key (`400 MISSING_API_KEY`), its endpoint is refused (`400 INVALID_URL`), or it sets an option only the deployment may set (`400 MODEL_CONFIG_INVALID`): tell the user to fix the server's model configuration;\n- the owner already has as many generations in progress as the server allows (`429 ACTIVE_RUN_LIMIT`; 2 by default, `OPENMAIC_MAX_ACTIVE_RUNS_PER_OWNER`): wait for a running job to finish, then submit again. A failed job whose run is paused (see below) does not count; retrying it does, so its Retry answers the same `429` while the owner is at the limit.\n\nThe request is checked in this order: the body (`400`/`413`), the models, the materials, then the limit.\n\n## Generation From Local Files\n\nUse this when the user wants the classroom built from their own files. Check `materials.formats` from the capabilities endpoint first when the file is not plain text, Markdown or PDF.\n\n1. Resolve the absolute path of each file.\n2. Confirm before reading the files.\n3. Upload each file as the raw request body (not multipart):\n\n```text\nPOST {url}/api/materials\nContent-Type: <the file's MIME type, e.g. application/pdf>\nX-Material-Filename: <the file name, percent-encoded if it is not ASCII>\n<raw file bytes>\n```\n\nFor example, with curl:\n\n```bash\ncurl -sS -c cookies.txt -b cookies.txt \\\n  -H 'Content-Type: application/pdf' \\\n  -H 'X-Material-Filename: lecture-notes.pdf' \\\n  --data-binary @/path/to/lecture-notes.pdf \\\n  {url}/api/materials\n```\n\nA successful upload answers `201` with `{ \"materialId\": \"...\", \"originalName\": \"...\", \"bytes\": ..., \"mime\": \"...\", \"mediaKind\": \"document\", \"extraction\": { \"status\": \"extracting\" } }` (`mediaKind` is `media` for audio and video). Other answers: `413` (the file exceeds the limit; the body's `maxBytes` gives it), `415` (unsupported type), `429` (the owner's material library is full — it holds a bounded number of files and bytes per owner, 100 files and 2 GiB by default; delete materials you no longer need, see below).\n\nThe server starts extracting the file (parsing a document, transcribing audio or video) right after the upload, in the background. Its state is on the material:\n\n```text\nGET {url}/api/materials/{materialId}\n```\n\nanswers `{ \"material\": { \"materialId\": \"...\", ..., \"extraction\": { \"status\": \"...\" } } }`, where `status` is:\n\n- `extracting` — still running;\n- `ready` — done; `textChars`, `pageCount` and `imageCount` say what it found, and `truncated` (when present) what a classroom leaves out of this file: `textChars` (only that many characters of its text are used) and `images` (`total` found, the first `max` looked at);\n- `failed` — `error` says why (for example the extraction service failed, or the file contains no text). `POST {url}/api/materials/{materialId}/extraction` extracts it again; or delete it and upload a fixed file.\n\nWaiting for `ready` before submitting is optional: a job whose material is still extracting waits for that extraction (it is not extracted twice), and a job whose material failed to extract fails with the extraction's error. Polling the material every few seconds before submitting lets you report a bad file before a job is created. Uploading the same file again reuses its finished extraction. `GET {url}/api/materials` (with no `sessionId` parameter) lists the owner's uploads with their extraction; with `sessionId` it lists an agent session's materials instead, and an empty `sessionId` answers `400`. The stored extraction counts against the owner's byte quota with the file.\n\nSubmit the job within a day of the upload: an upload that no job (and no agent session) uses is deleted once it has not been read for 24 hours (`OPENMAIC_UNUSED_MATERIAL_TTL_HOURS`); each `GET {url}/api/materials/{materialId}` counts as a read.\n\n4. Submit the job with the returned ids, in the order the documents should be read:\n\n```json\n{\n  \"requirement\": \"Create a classroom from these lecture notes\",\n  \"materialIds\": [\"mat_01...\", \"mat_02...\"]\n}\n```\n\nThe submission is checked before a job is created, and answers `400 INVALID_REQUEST` when:\n\n- an id is unknown, not fully uploaded, deleted, or belongs to someone else (`One or more materials are unavailable`, the same answer for all of these);\n- a material's type has no extractor available on this server;\n- the materials together exceed `maxTotalBytes`.\n\nIf a material cannot be extracted (its extraction failed, see above), the job fails rather than generating without it; surface the error to the user. Retrying such a job extracts the failed material again.\n\n5. After the job reaches `succeeded`, or `failed` with no Retry planned, delete the uploads you no longer need:\n\n```text\nDELETE {url}/api/materials/{materialId}\n```\n\nIt answers `200` with `{ \"materialId\": \"...\", \"deleted\": true }`, or a plain `404` for an id the owner does not have — including one already deleted, so a `404` after an earlier successful delete just means it is gone. Do not delete before the job is finished: the job reads the files when it runs, and a job whose material was deleted fails. Deleting frees the owner's library quota; with a shared team owner every caller shares that one quota, so cleaning up matters.\n\n### URLs Are Not Accepted\n\nThere is no way to pass a document URL. The server intentionally never fetches caller-supplied URLs; download the file locally (with the user's confirmation) and upload its bytes instead.\n\n## Polling Loop\n\nAfter the job is submitted:\n\n1. Save `jobId`, `pollUrl`, and `pollIntervalMs`.\n2. Do not submit another generation job while this one is still `queued` or `running`.\n3. Poll:\n\n```text\nGET {pollUrl}\n```\n\n4. Prefer a conservative polling cadence of about 60 seconds between polls for classroom generation jobs, even if `pollIntervalMs` is shorter.\n5. Treat `queued` and `running` as in-progress states.\n6. Stop only when `status` becomes `succeeded` or `failed` (`done` is then `true`).\n\n`step` is one of `queued`, `initializing` (extracting materials), `researching`, `generating_outlines`, `generating_scenes`, `generating_media` (the scenes are in; images and videos are finishing), `completed` or `failed`. `scenesGenerated` counts the scenes already in the classroom, and `totalScenes` appears once the outline exists.\n\n### Failed Jobs And Retry\n\nA `failed` job carries `error`, naming the step that failed when there is one (for example `scene:2:content: ...`). Two kinds exist:\n\n- The run is paused at a failed step (`retryable: true`, `runState: \"paused\"`). Nothing is lost: the scenes generated so far stay, and the classroom is read-only until the run completes. With the user's confirmation, re-run only that step by calling `POST {url}/api/generation-runs/{runId}/retry` with `{ \"commandId\": \"<a new unique id>\" }` (same owner, same cookie jar); the job then reads `running` again, so keep polling the same `pollUrl`. Do not resubmit the requirement instead: that starts a second classroom.\n- The classroom was deleted, or the run was discarded, before it finished (`error` says so). It cannot be retried.\n\n`GET {url}/api/generation-runs/{runId}` shows the run itself (its state, the failed step, every image and video) for the same owner.\n\n### Reliability Rules\n\n- Never restart the job just because a poll request fails once.\n- If a poll request returns a transient network error or `5xx`, wait about 60 seconds and retry the same `pollUrl`.\n- Treat a `404` on the `pollUrl` as terminal: the server does not know that job for this owner. Check that the poll carries the submission's cookie (or credential); if it does, stop polling, report the `jobId` to the user, and do not resubmit without their confirmation.\n- If the job is still running after many polls, tell the user it is still in progress and continue polling instead of resubmitting.\n- Prefer fewer poll attempts over aggressive polling. Long-running jobs are more likely to survive agent-loop limits if the tool-call cadence stays low.\n- Within a single agent turn, cap active polling to about 10 minutes. If the job is still not finished, tell the user it is still running and include the `jobId` and `pollUrl` so a later turn can continue checking without resubmitting.\n- Report progress to the user only when `status`, `step`, or visible progress meaningfully changes. Do not spam every poll result.\n- Do not try to recover from auth, provider, model, or base URL errors by changing request parameters. Tell the user to fix OpenMAIC server-side config and retry only after they confirm.\n- On `failed`, surface the server error and include the `jobId`.\n- On `succeeded`, read `result.classroomId` and `result.url` from the final poll response, and also read `result.warning` before telling the user the classroom is ready.\n  - If `result.warning` is set, quote it in the same update: some images or videos could not be generated (for example the provider refused them, or the owner's asset storage is full) and show a placeholder, or some speech clips were left without narration because the owner's asset storage refused them. The classroom URL is still usable. The retryable images and videos can be retried with `POST {url}/api/generation-runs/{runId}/retry` and `{ \"commandId\": \"<a new unique id>\", \"media\": { \"elementId\": \"<id>\" } }`, where the element ids and their states are in `GET {url}/api/generation-runs/{runId}` under `media`.\n  - A succeeded job is narrated when the server has a TTS provider configured (except the clips `result.warning` counts), and has no narration when it has none.\n\n## If The Loop Ends First\n\nIf the job is still running when you stop active polling for this turn, tell the user that the classroom generation is still running in the background and invite them to come back a little later to continue checking the same job.\n\nUse natural phrasing such as:\n\n```text\nThe classroom generation is still running in the background.\nJob ID: run-Q2xhc3Nyb29tSm9i\n\nCheck back with me in a little while and I can continue tracking this same job without starting over.\n```\n\n## What To Return\n\nReturn the generated classroom ID plus a directly clickable classroom URL.\n\nWhen the succeeded job includes `result.warning`, say that some images, videos or narration are missing in the same reply, quoting `result.warning`, and still include the classroom ID and URL.\n\nOutput the URL as a raw absolute URL on its own line.\n\nDo not wrap the URL in:\n\n- bold markers such as `**...**`\n- markdown links such as `[title](url)`\n- code formatting such as `` `...` ``\n- angle brackets such as `<...>`\n- markdown tables\n\nUse a compact format like:\n\n```text\nClassroom ID: Uyh82Y32ZK\nClassroom URL:\nhttp://localhost:3001/classroom/Uyh82Y32ZK\n```\n\nIf the job fails, return the job ID plus the server error, and say whether it can be retried (a paused run) or not (a deleted classroom or a discarded run).\n\nIf generation fails, surface the server error directly instead of paraphrasing it away.\n\nIf the error suggests a provider or model configuration problem, explicitly tell the user to update `openmaic.yml` (with the key in `.env.local`) or the model settings in the web app instead of attempting a runtime override. See [provider-keys.md](provider-keys.md#recognizing-configuration-errors) for the common messages.\n\n## Confirmation Requirements\n\n- Ask before reading local files for upload.\n- Do not ask for a second confirmation before the generation request if the user has already clearly asked you to generate the classroom.\n\nFile v0.3.11:references/live-demo.md\n\n# Live Demo Mode\n\nThe OpenMAIC Live Demo (open.maic.chat) is the cloud edition — the version officially deployed and hosted by the OpenMAIC team, so no local setup is required. Use this when the user has an access code from open.maic.chat and wants to skip local setup.\n\n## Access Code Setup\n\n1. Read `accessCode` from skill config (`~/.openclaw/openclaw.json` → `skills.entries.openmaic.config.accessCode`).\n2. If found, use it directly. Do not ask the user to paste the code into chat.\n3. If not found, tell the user how to get an access code and where to put it:\n   - Get your access code: sign in at https://open.maic.chat, click your account in the top-right corner, open \"访问码设置\" (access code settings), and generate a code (starts with `sk-`).\n   - Add it to the config file: edit `~/.openclaw/openclaw.json` and set `skills.entries.openmaic.config.accessCode` to your access code.\n   Wait for the user to confirm before continuing. Do not ask them to paste the code in chat.\n4. Verify connectivity: `GET https://open.maic.chat/api/health` with `Authorization: Bearer <access-code>`\n   - On success: confirm connection and proceed to generation.\n   - On failure (401): access code is invalid, ask the user to check or regenerate at open.maic.chat and update the config file.\n   - On failure (network): suggest checking network or trying local mode.\n\n## Generating a Classroom\n\nFollow the same generation flow as [generate-flow.md](generate-flow.md) with these differences:\n\n- **Base URL**: `https://open.maic.chat` (hardcoded, not configurable)\n- **Authorization**: Include header `Authorization: Bearer <access-code>` on all API requests\n- **Classroom URL**: `https://open.maic.chat/classroom/{id}`\n\n### Capabilities in Live Demo Mode\n\nOptional features (web search, image and video generation, TTS) follow the Live Demo server's configuration; there are no request flags for them. To see which features a job is configured to attempt and which file types it can use, query `GET /api/generate-classroom/capabilities` on the Live Demo base URL (with the auth header). Upload local files with `POST /api/materials` and pass the returned ids as `materialIds`, then delete them after the job finishes, exactly as in [generate-flow.md](generate-flow.md). Send the same `Authorization` header on every request of the flow (uploads, submission, polls, deletions). The Live Demo instance may update on a different schedule than the local codebase: if the capabilities endpoint answers `404`, the instance predates this contract.\n\n## Quota\n\n- 10 generations per day, independent of web UI quota\n- If generation returns 403 with `Daily quota exhausted`, inform the user of the daily limit and that it resets at midnight.\n\n## Error Handling\n\n| HTTP Status | Meaning | Action |\n|-------------|---------|--------|\n| 401 | Invalid access code | Ask user to check their code or generate a new one at open.maic.chat |\n| 403 | Quota exhausted | Inform daily limit (10), suggest trying tomorrow |\n| 500 | Server error | Suggest retrying later or switching to local mode |\n\nFile v0.3.11:references/provider-keys.md\n\n# Provider Keys\n\n## Critical Boundary\n\nOpenMAIC generation does not automatically reuse the OpenClaw agent's current model or API key.\n\nOpenMAIC resolves every model and key on the server, from its own model configuration:\n\n- `openmaic.yml` (written by the operator; path overridable with `OPENMAIC_CONFIG`) declares providers and assigns models to capability slots. Keys stay in `.env.local` and are referenced from the file as `${VAR}`.\n- The model settings in the OpenMAIC web app (**Settings → Token Plan**, **Model Services** and **Course Model Config**) edit the slots and providers `openmaic.yml` leaves open, for the current workspace.\n\nThis skill does not rely on runtime overrides for model, provider, API key, base URL, or provider type. The old request headers (`x-model`, `x-api-key`, `x-base-url`, `x-model-routes`, `x-*-provider`, …) are deprecated and ignored once a slot is configured; never use them as a workaround.\n\nIf the user wants to change the model or provider, they edit `openmaic.yml` (and `.env.local` for the key) or use the model settings in the web app.\n\n## Interaction Flow\n\n1. Recommend one provider path first (see \"Recommendation Paths\" below). Do not start by asking for an API key.\n2. Ask whether the user wants to configure it in `openmaic.yml` + `.env.local` (recommended for self-hosting and anything reproducible) or in the web app's model settings after starting (simplest for a personal install).\n3. Tell the user exactly which file and fields to edit — they edit the files themselves. Do not offer to write the key for them, do not ask for the literal key in chat, and do not suggest temporary request-time overrides.\n4. Wait for the user to confirm they finished editing before continuing. `openmaic.yml` is read at startup: a running server must be restarted after it changes.\n5. If startup or generation later fails because of auth, provider, or model selection, direct the user back to the same configuration and wait for confirmation before retrying.\n\n## The Configuration File\n\nStart from the example in the repository:\n\n```bash\ncp .env.example .env.local\ncp openmaic.example.yml openmaic.yml\n```\n\nAs shipped, the example has one active provider (`openai`, reading `OPENAI_API_KEY`) and the `llm` slot; everything else is commented out. Startup refuses any `${VAR}` that is not set, so tell the user to either set that one key or replace the provider with the path they chose below, and to uncomment optional blocks only together with the variables they name.\n\nThree concepts:\n\n- **Provider** — an account the server can call: an id the user chooses, a `preset` (which vendor), and `apiKey: ${VAR}`.\n- **Slot** — a use of AI. `llm` is the default chat model for everything; `course.outline`, `course.content`, `course.content.slide`, `classroom`, `agent`, … override it for one use; `tts`, `asr`, `image`, `video`, `webSearch` and `document` are the media and tool capabilities.\n- **Assignment** — `slot: <provider id>:<model id>`, or `<provider id>` alone for search/document/media providers (their default model), or `null` to turn the capability off.\n\nMinimal file:\n\n```yaml\nproviders:\n  anthropic:\n    preset: anthropic\n    apiKey: ${ANTHROPIC_API_KEY}\n\nslots:\n  llm: anthropic:claude-sonnet-4-6\n```\n\nwith `ANTHROPIC_API_KEY=sk-ant-...` in `.env.local`.\n\nSlots written in `openmaic.yml` are server defaults that users may still change in the model settings; slots left out follow their parent (`llm` for chat slots). To fix slots for everyone, list them under `lock` (`lock: all` fixes every slot); `allowUserKeys: false` keeps users from adding keys or token plans of their own.\n\n## Recommendation Paths\n\n### 1. One Key for Everything (token plan)\n\nRecommended when the user wants illustrations, narration, video and web search with the least setup. A token plan preset covers several capabilities with one key:\n\n```yaml\nproviders:\n  minimax:\n    preset: minimax\n    apiKey: ${MINIMAX_API_KEY}\n\nslots:\n  llm: minimax:MiniMax-M3\n  image: minimax\n  video: minimax\n  tts: minimax\n  webSearch: minimax\n```\n\nOther token plan presets: `tokendance`, `volcengine-ark`, `kimi-coding-plan` (chat only).\n\n### 2. A Single Chat Vendor\n\nRecommended when the user already has a key for one vendor:\n\n```yaml\nproviders:\n  google:\n    preset: google\n    apiKey: ${GOOGLE_API_KEY}\n\nslots:\n  llm: google:gemini-2.5-flash\n```\n\nSame shape for `openai` (`OPENAI_API_KEY`), `anthropic`, `deepseek`, `qwen`, `glm`, `kimi`, `openrouter` and the other chat presets.\n\n### 3. Web App Only\n\nFor a personal install where the user does not want to edit files: start OpenMAIC, open **Settings → Token Plan** and enter a plan key, or **Settings → Model Services** and enter a service key. The key is entered in the browser, stored encrypted on the server, and never shown again. Nothing to edit in `openmaic.yml`.\n\n### 4. Existing Environment-Variable Setups\n\nA deployment configured through provider variables (`OPENAI_API_KEY`, …), `server-providers.yml`, `DEFAULT_MODEL` and `MODEL_FALLBACK` still works while there is no `openmaic.yml`; the server translates it at startup and logs a deprecation notice. Recommend moving to `openmaic.yml` when the user touches the configuration anyway. `MODEL_ROUTES` is no longer read: a server that sets it without `openmaic.yml` refuses to start, and the per-stage models must be written as slots (see the Configuration docs, \"Migrating from the legacy configuration\").\n\n## Model Reference Rule\n\nIn `openmaic.yml`, a chat slot always names `<provider id>:<model id>`, where the provider id is the key under `providers` (not necessarily the preset):\n\n- `google:gemini-2.5-flash`\n- `anthropic:claude-sonnet-4-6`\n- `openai:gpt-5.4-mini`\n- `deepseek:deepseek-v4-flash`\n\nA chat slot with a provider id alone (`llm: openai`) is refused at startup. Non-chat slots may use the provider id alone.\n\nThe exact model IDs above are examples. Model names change as providers release new versions — if a model ID is rejected by the provider, direct the user to the provider's official docs (or the Supported models page) for the current name.\n\n## Optional Features\n\nThese features need their own slot assigned, usually with a provider of their own. Ask the user if they want any of them after the chat model works. Each provider is declared once under `providers` and assigned to its slot.\n\n| Feature | Slot | Example presets |\n|---------|------|-----------------|\n| Web Search | `webSearch` | `tavily`, `exa`, `bocha`, `brave`, `baidu` |\n| Image Generation | `image` | `seedream`, `qwen-image`, `nano-banana`, `openai-image` |\n| Video Generation | `video` | `seedance`, `kling`, `veo`, `minimax-video` |\n| TTS | `tts` | `openai-tts`, `azure-tts`, `glm-tts`, `qwen-tts`, `minimax-tts` |\n| Speech Recognition | `asr` | `openai-whisper`, `qwen-asr`, `funasr-asr` |\n| Document Parsing | `document` | `mineru-cloud`, `mineru`, `alidocmind` |\n\nExample:\n\n```yaml\nproviders:\n  tavily:\n    preset: tavily\n    apiKey: ${TAVILY_API_KEY}\n  seedream:\n    preset: seedream\n    apiKey: ${IMAGE_SEEDREAM_API_KEY}\n\nslots:\n  webSearch: tavily\n  image: seedream\n```\n\nThese are all optional. Classroom generation works without them — they only unlock richer content. To turn one off explicitly, set its slot to `null`.\n\n## Recognizing Configuration Errors\n\n- **Server exits at startup with `Invalid model configuration in …/openmaic.yml`** — the message lists each problem with its path (an unset `${VAR}`, an unknown preset or slot, a provider not declared under `providers`, a chat slot without a model id). Relay it to the user unchanged.\n- **`MODEL_ROUTES does not carry over to the model configuration …`** — the user must write the per-stage models as slots in `openmaic.yml` and remove `MODEL_ROUTES`.\n- **`No model is configured for <slot>`** — assign that slot (or its parent, e.g. `llm`) in `openmaic.yml` or the model settings.\n- **`The <slot> capability is turned off`** — the slot is `null`; the operator turned it off on purpose.\n\n## Recommended Prompts To The User\n\nExample phrasing the agent can adapt:\n\n- \"I recommend configuring OpenMAIC with `openmaic.yml`: copy `openmaic.example.yml`, keep your key in `.env.local`, and tell me when you're done.\"\n- \"For the least setup, one token plan key covers chat, images, narration, video and search. If you already have an Anthropic or Google key, a single `llm` line is enough. Which path do you want?\"\n\nThe \"do not ask for the key in chat / do not offer to write it\" rules are covered in [Interaction Flow](#interaction-flow) above — do not open by requesting the key.\n\nFile v0.3.11:references/startup-modes.md\n\n# Startup Modes\n\n## Goal\n\nHelp the user choose how OpenMAIC should run before you start anything.\n\n## Options\n\n### 1. Development Mode\n\nRecommended for first-time setup and debugging. Courses are stored in PostgreSQL and the server refuses to start without `DATABASE_URL`, so start the local development database first (a separate PostgreSQL in Docker on `127.0.0.1:5432`) and uncomment the local `DATABASE_URL` line in `.env.local`:\n\n```bash\npnpm db:up\npnpm dev\n```\n\nTradeoff:\n\n- Fastest feedback loop\n- Best for validating config changes\n- Not representative of production startup\n\n### 2. Production-Like Local Mode\n\nRecommended when the user wants behavior closer to a deployed server. Needs `DATABASE_URL` too (`pnpm db:up` locally).\n\n```bash\npnpm build && pnpm start\n```\n\nTradeoff:\n\n- Closer to production\n- Slower startup than `pnpm dev`\n\n### 3. Docker Compose\n\nUse only when the user explicitly wants containerized startup or wants to avoid local Node setup details.\n\n```bash\ndocker compose up --build\n```\n\nThis starts the app and PostgreSQL (courses are stored server-side) in single-user mode, published on `127.0.0.1:3000` only. To reach it from other machines, start with `OPENMAIC_PUBLISH_ADDRESS=0.0.0.0` and set `ACCESS_CODE` in `.env.local` first; without an access code anyone who can reach it shares, edits and can delete the single library (the server warns at startup but still runs).\n\nTradeoff:\n\n- Cleaner isolation\n- Heavier and slower\n- Harder to debug application-level issues quickly\n\n## Recommendation Order\n\n1. `pnpm dev`\n2. `pnpm build && pnpm start`\n3. `docker compose up --build`\n\n## Health Check\n\nAfter startup, verify:\n\n```bash\ncurl -fsS http://localhost:3000/api/health\n```\n\nIf the skill config provides a custom `url`, use that instead.\n\n## Confirmation Requirements\n\n- Ask the user to choose one startup mode.\n- Ask again before running the selected command.\n\nFile v0.3.11:skill-card.md\n\n## Description:\n\nGuides agents through OpenMAIC setup, interactive classroom generation, and product or SDK customization.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[wyuc](https://clawhub.ai/user/wyuc)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and educators use this skill to set up or access OpenMAIC, generate interactive classrooms from prompts and approved materials, and extend its application or SDK.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: A stored OpenMAIC access code may be used for authenticated requests.\n\nMitigation: Keep access codes private and confirm the target service before connecting.\n\nRisk: Approved classroom materials may be uploaded to the selected server.\n\nMitigation: Confirm the server URL and review files before approving uploads.\n\nRisk: Local setup commands may install dependencies or change a checkout.\n\nMitigation: Run setup commands only in a checkout you trust.\n\n## Reference(s):\n\n- [OpenMAIC skill release](https://clawhub.ai/wyuc/skills/openmaic)\n- [OpenMAIC Live Demo](https://open.maic.chat)\n- [Classroom generation guide](artifact/references/generate-flow.md)\n- [Provider configuration guide](artifact/references/provider-keys.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration instructions, Code]\n\n**Output Format:** [Markdown with commands and code snippets]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May provide a classroom link after generation.]\n\n## Skill Version(s):\n\n0.3.11 (source: ClawHub release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.3.10: 11 files, 27397 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7779b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (13554b), references/live-demo.md (3075b), references/provider-keys.md (8432b), references/startup-modes.md (1893b), skill-card.md (2093b), SKILL.md (6669b), _meta.json (128b)\n\nFile v0.3.10:SKILL.md\n\n---\nname: openmaic\ndescription: OpenMAIC assistant for setting up, generating, and extending OpenMAIC. Use when the user wants to use OpenMAIC, generate a multi-agent interactive classroom, or build on / extend / customize OpenMAIC and its @openmaic/* SDK (secondary development, 二开) — covers Live Demo or local setup, startup modes, provider keys, classroom generation, and secondary development (forking, providers/storage/themes, routes, or the renderer/editor).\nuser-invocable: true\nmetadata: { \"openclaw\": { \"emoji\": \"🏫\" } }\n---\n\n# OpenMAIC Skill\n\nUse this as a guided, confirmation-heavy SOP. Do not compress the whole setup into one reply and do not perform state-changing actions without explicit user confirmation.\n\n## Core Rules\n\n- Move one phase at a time.\n- Before any state-changing action, ask for confirmation.\n- If local state already exists, show what you found and ask whether to keep it.\n- Do not assume the OpenClaw agent's own model or API key will be reused by OpenMAIC.\n- OpenMAIC classroom generation uses OpenMAIC's server-side model configuration (`openmaic.yml` and the model settings in the web app).\n- This skill must not rely on any request-time model or provider overrides.\n- Only that server-side configuration may control provider selection and defaults.\n- Do not default to asking the user to paste API keys into chat.\n- Prefer guiding the user to edit local config files themselves.\n- Do not offer to write API keys into config files on the user's behalf.\n- Once setup is complete and the user clearly asks to generate a classroom, do not ask for a second confirmation before submitting the generation job.\n- Keep confirmations for local file reads such as reading a PDF from disk before uploading it.\n\n## Optional Skill Config\n\nIf present, read defaults from `~/.openclaw/openclaw.json` under:\n\n```jsonc\n{\n  \"skills\": {\n    \"entries\": {\n      \"openmaic\": {\n        \"enabled\": true,\n        \"config\": {\n          \"accessCode\": \"sk-xxx\",\n          \"repoDir\": \"/path/to/OpenMAIC\",\n          \"url\": \"http://localhost:3000\"\n        }\n      }\n    }\n  }\n}\n```\n\n- If `accessCode` is present, default to Live Demo mode and skip the mode-selection prompt — unless the user's intent is to extend/build on OpenMAIC (see the exception in Phase 0).\n- Use `repoDir` and `url` only as defaults for local mode.\n- Still confirm before acting.\n\n## SOP Phases\n\n### 0. Choose Mode\n\nFirst check skill config for `accessCode`. If present, announce that a stored access code was found and proceed directly to Live Demo mode (load [references/live-demo.md](references/live-demo.md), skip phases 1–4). Do not ask the user to paste the code again. **Exception:** if the user's stated intent is to extend / build on / customize OpenMAIC or consume the `@openmaic/*` SDK (二次开发 / 二开 / SDK), do not auto-shortcut — go to the extend branch below regardless of `accessCode`. A returning Live Demo user who now wants to do 二开 should be routed to extend, not silently sent back to Live Demo.\n\nIf no `accessCode` in config (or the extend exception above applies), ask the user how they want to use OpenMAIC:\n\n1. **Use the OpenMAIC Live Demo** (recommended for quick start) — The cloud edition: the version officially deployed and hosted by the OpenMAIC team at open.maic.chat. Requires an access code (starts with `sk-`). Get yours by signing in at https://open.maic.chat, clicking your account in the top-right corner, opening \"访问码设置\" (access code settings), and generating a code; then add it to `~/.openclaw/openclaw.json` under `skills.entries.openmaic.config.accessCode`. No local setup needed.\n2. **Run locally** — Clone the repo, configure provider keys, and run on your machine.\n3. **Extend or build on OpenMAIC (二次开发)** — Fork the repo and customize the product, or consume the `@openmaic/*` SDK to build something new.\n\nIf the user chooses Live Demo mode, load [references/live-demo.md](references/live-demo.md) and skip phases 1–4.\nIf the user chooses local mode, proceed to phase 1 as usual.\nIf the user chooses to extend/build on OpenMAIC, load [references/extend.md](references/extend.md) and skip the setup/generation phases.\n\n### 1. Clone Or Reuse Existing Repo\n\nLoad [references/clone.md](references/clone.md).\n\nUse this when the user has not installed OpenMAIC yet or when you need to confirm which local checkout to use.\n\n### 2. Choose Startup Mode\n\nLoad [references/startup-modes.md](references/startup-modes.md).\n\nUse this after the repo location is confirmed. Present the available startup modes, recommend one, and wait for the user's choice.\n\n### 3. Configure Provider Keys\n\nLoad [references/provider-keys.md](references/provider-keys.md).\n\nUse this before starting classroom generation. Recommend a provider path and tell the user exactly what to edit themselves (`openmaic.yml` and `.env.local`, or the model settings in the web app). If generation later fails due to provider/model/auth issues, return to this phase and direct the user to update the same configuration.\n\nAfter the core LLM key is configured, ask the user if they want to enable optional features (web search, image generation, video generation, TTS). Each is a slot with its own provider — see the \"Optional Features\" section in provider-keys.md.\n\n### 4. Start And Verify OpenMAIC\n\nAfter the user has chosen a startup mode and configured keys, start OpenMAIC using the chosen method, then verify the service with `GET {url}/api/health`.\n\n### 5. Generate A Classroom\n\nLoad [references/generate-flow.md](references/generate-flow.md).\n\nUse this only after the service is healthy. Confirm before reading local files to upload. If the user has already clearly asked to generate, do not ask for a second confirmation before submitting the generation job, and then follow the polling loop until it succeeds or fails. Only send the supported fields (`requirement`, `materialIds`) for generation requests; optional features follow the server's provider config. Uploads and the submission must resolve to the same owner: in anonymous-cookie mode reuse the same cookie jar on every request. For long-running jobs, prefer sparse polling and tell the user to check back later if the turn ends before completion.\n\n## Response Style\n\n- Keep each step short and explicit.\n- Prefer 2-3 concrete options when the user must choose.\n- Always include the recommended option first and explain why in one sentence.\n- After a step completes, say what changed and what the next confirmation is for.\n- When returning a classroom link, place the raw absolute URL on its own line with no bold, markdown link syntax, code formatting, or tables.\n\nFile v0.3.10:_meta.json\n\n{\n  \"ownerId\": \"kn7cjfn5dkygrges9scanxv97d82x3t1\",\n  \"slug\": \"openmaic\",\n  \"version\": \"0.3.10\",\n  \"publishedAt\": 1790943216908\n}\n\nFile v0.3.10:references/clone.md\n\n# Clone Or Reuse Existing Repo\n\n## Goal\n\nEstablish which OpenMAIC checkout will be used for setup and runtime actions.\n\n## Procedure\n\n1. Check whether OpenMAIC already exists locally.\n2. If a checkout exists, show the path and ask whether to reuse it.\n3. If no checkout exists, propose cloning the repo and ask for confirmation.\n4. After clone, confirm dependency installation separately.\n\n## Recommended Path\n\n- Recommended: reuse an existing checkout if it is already on the target branch.\n- Otherwise: clone a fresh checkout from GitHub, then install dependencies.\n\n## Commands\n\nClone:\n\n```bash\ngit clone https://github.com/THU-MAIC/OpenMAIC.git\ncd OpenMAIC\n```\n\nInstall dependencies:\n\n```bash\npnpm install\n```\n\n## Confirmation Requirements\n\n- Ask before `git clone`.\n- Ask before `pnpm install`.\n- If the repo is dirty, tell the user and ask whether to continue with that checkout.\n\nFile v0.3.10:references/extend-cookbook.md\n\n# Extend The OpenMAIC Product (Cookbook)\n\n## Scope\n\nYou are working **inside a fork of the OpenMAIC product** (the Next.js app at the repo root), customizing it in place. If instead you want to consume `@openmaic/*` in a separate app, use [extend-sdk.md](extend-sdk.md) instead.\n\nEntry points below are given as **file + symbol name** (not line numbers — they drift). Read the file, then jump to the symbol. The `@/*` path alias is anchored at the repo root.\n\n## Task 1 — Swap Or Add An AI Provider\n\n**Goal:** route generation to a different provider, or register a brand-new one.\n\nTwo distinct cases:\n\n- **Use an already-supported provider** (it's in the union below): no source change. Declare it in `openmaic.yml` with its preset and assign it to a slot, key in `.env.local` as `${VAR}` — follow [provider-keys.md](provider-keys.md). Chat slots always name `<provider id>:<model id>`.\n- **Register a NEW provider** (source change):\n  1. Add the id to the `BuiltInProviderId` union in `lib/types/provider.ts`.\n  2. Register its config + models in the `PROVIDERS` registry in `lib/ai/providers.ts`.\n  3. The registry entry becomes a preset automatically (`lib/config/provider-presets.ts`; the preset id is the registry id unless `lib/config/preset-ids.ts` overrides it), so `openmaic.yml` can declare it with `preset: your-id` and the model settings offer it. No env wiring is needed.\n  4. Optional, legacy only: to also accept `<PREFIX>_API_KEY` / `_BASE_URL` / `_MODELS` from the environment without `openmaic.yml`, add a `PREFIX: 'your-id'` entry to `LLM_ENV_MAP` in `lib/server/provider-config.ts`. That path is deprecated.\n  5. A new token plan (one key, several capabilities) is one entry in `lib/config/token-plan-presets.ts`; it becomes a preset whose recommended models fill the slots it covers in the first-run setup.\n\n**Gotcha:** OpenMAIC has **no hardcoded model fallback**. If no model is assigned to the `llm` slot (or the more specific slot a call uses), generation fails with `No model is configured for <slot>` rather than picking a vendor — always assign one.\n\n## Task 2 — Server-Side Persistence (PostgreSQL / S3)\n\n**Goal:** understand where documents, runtime state and assets live, and point them at your database.\n\nAccurate topology (there is **no local-file backend**, and no browser-storage mode):\n\n- **Always server-backed:** documents, runtime state and assets are stored on the server. The server **refuses to start without `DATABASE_URL`**; locally, `pnpm db:up` starts a separate development PostgreSQL (its own Compose project and volume) on `127.0.0.1` and `.env.example` carries the matching `DATABASE_URL`.\n- **Client side:** `lib/persistence/bootstrap.ts` configures the browser's HTTP-backed `HttpRuntimeStore` / `HttpDocumentStore` / `HttpAssetStore`, which call `/api/persistence`. Those calls carry no credential of their own: the server attributes them to the owner the owner identity seam (`lib/server/identity/`) resolves, and the runtime learner key is that owner id. Only device-local state (settings, playback position, local media cache) stays in the browser (`lib/device-storage/`).\n- **Server side:** the `/api/persistence` catch-all (`app/api/persistence/[...path]/route.ts`) persists **documents + runtime to PostgreSQL**, and **asset bytes to PostgreSQL or S3**. The byte-layer selection lives in `lib/persistence/asset-byte-store.ts` (`configuredS3Bucket` / `lazyAssetByteStore`) and is strictly three-way: **unset/empty** `ASSET_S3_BUCKET` ⇒ `PgAssetByteStore`; a **valid** bucket ⇒ S3; an **invalid** bucket name ⇒ asset operations **fail** — validation throws, there is no fallback to PG. (The failure isn't cached: the next asset request retries, and only asset traffic is affected — document/runtime requests keep working.)\n- The backends themselves come from `@openmaic/storage` subpaths (`@openmaic/storage/document/pg`, `@openmaic/storage/runtime/pg`, `@openmaic/storage/asset/pg-bytes`, `@openmaic/storage/asset/s3-bytes`) — see the storage table in [extend-sdk.md](extend-sdk.md).\n\n**Gotcha:** S3 additionally needs `@aws-sdk/client-s3` (optional peer of `@openmaic/storage`) installed in the app, and PG needs a reachable Postgres + the package's schema-ensure step. The removed `NEXT_PUBLIC_PERSISTENCE` build switch is ignored.\n\n## Task 3 — Branding / UI / Theme\n\n**Goal:** change title, fonts, color tokens, or preset themes.\n\nEntry points:\n\n- Title + fonts: `app/layout.tsx` (the title metadata; fonts include `@openmaic/renderer/fonts.css`).\n- Design tokens: `app/globals.css` — Tailwind v4 (`@import 'tailwindcss'`, the `@source` directives that scope the renderer's classes, and the `@theme inline { … }` block for custom tokens).\n- Preset themes: the `PRESET_THEMES` array in `configs/theme.ts`.\n\n**Gotcha:** This is Tailwind **v4** — config lives in CSS via `@theme`/`@source`, **not** in `tailwind.config.js`. The renderer's class names are discovered through the `@source` scans of its `dist`; keep token names consistent across `@theme` and the renderer or styles silently drop.\n\n## Task 4 — Add A Page Or API Route\n\n**Goal:** ship a new screen or a new server endpoint.\n\nEntry points:\n\n- Page (App Router): `app/<segment>/page.tsx`.\n- API route: `app/api/<segment>/route.ts`.\n- Reuse the shared server helpers instead of reimplementing: `lib/server/api-response.ts`, `lib/server/llm-error-response.ts`, `lib/server/ssrf-guard.ts`, `lib/server/proxy-fetch.ts`.\n- Import app code via the `@/*` alias (e.g. `import { … } from '@/lib/…'`).\n\n**Gotcha:** Any route that fetches a user-supplied URL must go through `lib/server/ssrf-guard.ts` and `lib/server/proxy-fetch.ts` — do not call `fetch()` directly with attacker-controlled hosts. LLM error responses should go through `llm-error-response.ts` to stay consistent with the rest of the API.\n\n## Task 5 — Embed The Renderer Or Editor\n\n**Goal:** drop a slide canvas (read-only) or the full editable slide surface into a component.\n\nEntry points (real usage examples to copy):\n\n- Render-only `SlideCanvas` from `@openmaic/renderer`: see `components/slide-renderer/SlideThumbnail.tsx`.\n- Editable surface `EditableSlideCanvasWithUI` from `@openmaic/editor/ui`: see `components/edit/surfaces/slide/RendererEditorCanvas.tsx`.\n- Types for slide data: `Slide`, `PPTElement` from `@openmaic/dsl`.\n\n**Gotcha (CSS is mandatory):** The renderer renders unstyled/broken without its fonts and Tailwind classes. In the consuming layout/globals: `@import '@openmaic/renderer/fonts.css';` (or the JS import form), and ensure the renderer's classes are in Tailwind v4's content scan (an `@source` directive pointing at the renderer's `dist`). Note `fonts.css` is **generated** (regen via the renderer's `genfonts` script) and self-hosts CJK faces by fetching woff2 on demand from `https://file.maic.chat/fonts/<name>.woff2` — relevant for offline/custom-font operation. The editor transitively requires the renderer's full peer stack (Tailwind v4, `motion`, optionally `echarts`/`shiki`) — not just `react`/`react-dom`.\n\n## After Your Edits — Run And Verify\n\nThis product still runs like the stock app; don't reinvent the startup steps:\n\n- Dev server / startup mode → [startup-modes.md](startup-modes.md).\n- Provider keys the running server needs → [provider-keys.md](provider-keys.md).\n- Verify with `GET {url}/api/health` (Phase 4), then confirm UI/route changes in the browser.\n\nRebuild sequence when you touch `packages/@openmaic/*` source: consumers resolve to the built `dist/`, not `src/`, so rebuild the changed package (dependency order: `dsl → generation → storage → importer → renderer → editor`). `pnpm install`'s postinstall already does this in order; for a single package use its `pnpm run build`.\n\nFile v0.3.10:references/extend-sdk.md\n\n# Consume The @openmaic/* SDK (Build A New App)\n\n## Scope\n\nYou are building a **separate app** that consumes `@openmaic/*` packages via `npm install` — not working inside the OpenMAIC monorepo. If you are customizing the OpenMAIC product itself, use [extend-cookbook.md](extend-cookbook.md) instead.\n\nThe SDK is early-stage (`0.x`). Versions below are current as of this writing — confirm against the registry when you install.\n\n## Package Quick Reference\n\n| Package | Version | Purpose | Key Exports | Peer Deps |\n|---|---|---|---|---|\n| `@openmaic/dsl` | 0.8.0 | The slide **contract** — types + JSON schema. Zero runtime deps. | `Slide`, `PPTElement`, `./schema/*` | none |\n| `@openmaic/renderer` | 0.1.0 | Render slides to DOM (read-only). | `SlideCanvas`, `./snapshot`→`slideToPng` | react ≥18, react-dom ≥18, motion ≥11, tailwindcss ≥4; **optional:** echarts ≥5, shiki ≥1 |\n| `@openmaic/editor` | 0.0.2 | Editable slide surface + prosemirror editor. | `EditableSlideCanvasWithUI` (from `./ui`) | react ≥18, react-dom ≥18 **+ renderer's full peer stack transitively** |\n| `@openmaic/generation` | 0.3.0 | LLM-driven scene/lesson generation. | `generateSceneContent` | none |\n| `@openmaic/storage` | 0.2.5 | Document / Runtime / Asset / KV stores (Browser · HTTP · PG · S3). | see table below | **optional:** `@aws-sdk/client-s3` |\n| `@openmaic/importer` | 0.1.2 | Import PPTX / PDF into the DSL. | `importPptx` | none |\n\n> `editor` is `0.0.2`. Its own `peerDependencies` list only `react`/`react-dom`, but it `dependencies` on `@openmaic/renderer`, so you must also satisfy the renderer's peers (tailwindcss v4, motion, and optionally echarts/shiki) or it breaks at render time.\n\n## Minimal Starter — Render A Slide\n\n1. Install peers and the package (pin exact versions for `0.x`):\n   ```bash\n   npm i @openmaic/dsl@0.8.0 @openmaic/renderer@0.1.0 \\\n         react@^18 react-dom@^18 motion@^11 tailwindcss@^4\n   # only if you render charts / code-highlighted blocks:\n   npm i echarts@^5 shiki@^1\n   ```\n2. Mandatory CSS — without it the canvas renders unstyled:\n   ```css\n   @import 'tailwindcss';\n   @import '@openmaic/renderer/fonts.css';\n   ```\n   ...and ensure Tailwind v4 scans the renderer's classes, e.g. `@source '../node_modules/@openmaic/renderer/dist';` in your globals. (`fonts.css` is generated and fetches CJK woff2 on demand from `https://file.maic.chat/fonts/<name>.woff2` — relevant for offline/custom-font needs.)\n3. Render:\n   ```tsx\n   import { SlideCanvas } from '@openmaic/renderer';\n   import type { Slide } from '@openmaic/dsl';\n   ```\n   For a working example of props/usage, read `components/slide-renderer/SlideThumbnail.tsx` in the OpenMAIC repo.\n\n## Precise Import Paths\n\n| Want | Import |\n|---|---|\n| Slide / element types | `@openmaic/dsl` (`Slide`, `PPTElement`) |\n| JSON schema (validation) | `@openmaic/dsl/schema/*` |\n| Render a slide | `@openmaic/renderer` (`SlideCanvas`) |\n| Snapshot a slide → PNG | `@openmaic/renderer/snapshot` (`slideToPng`) |\n| Editable slide surface | `@openmaic/editor/ui` (`EditableSlideCanvasWithUI`) |\n| Generate lesson content | `@openmaic/generation` (`generateSceneContent`) |\n| Import a PPTX | `@openmaic/importer` (`importPptx`) |\n| Storage backends | see table below |\n\n## Storage Backends — Exact Subpaths\n\nThere is **no** bare `@openmaic/storage/document`, `/runtime`, or `/asset` subpath — always use the **backend-suffixed** form. The main barrel (`@openmaic/storage`) is asymmetric:\n\n| Domain | Browser | HTTP | PG | S3 |\n|---|---|---|---|---|\n| **Document** | barrel `.` | `./document/http` | `./document/pg` | — |\n| **Runtime** | barrel `.` | `./runtime/http` ⚠️ | `./runtime/pg` ⚠️ | — |\n| **Asset** | barrel `.` | `./asset/http` | `./asset/pg`, `./asset/pg-bytes` | `./asset/s3-bytes` |\n| **KV** | barrel `.` | `./kv/http` | — | — |\n| Server helpers | `./server`, `./server/reference` | | | |\n\n⚠️ **Runtime asymmetry (the main gotcha):** `HttpRuntimeStore` and `PgRuntimeStore` are **only** reachable via `@openmaic/storage/runtime/http` and `@openmaic/storage/runtime/pg` — they are **not** in the main barrel. `BrowserRuntimeStore` is. Document/Asset/KV backends are all in the barrel. Importing `PgRuntimeStore` from `@openmaic/storage` will fail with \"not exported\".\n\nPG backends ship an `ensureSchema` (plus a `*_PG_SCHEMA` constant) you must run once. S3 needs `@aws-sdk/client-s3` installed.\n\n## Version Pinning\n\nAll six packages are `0.x`. Pin **exact** versions (e.g. `\"@openmaic/renderer\": \"0.1.0\"`), not `^` ranges — `0.x` semver treats minor bumps as breaking, and these packages are still moving fast.\n\n## If You Need To Change The SDK Itself\n\nConsuming via `npm install` means you treat the SDK as a black box. To modify SDK behavior, the path is heavier:\n\n1. Fork `THU-MAIC/OpenMAIC`, edit the package source under `packages/@openmaic/*`, rebuild its `dist/`.\n2. Produce an installable artifact for **just that subpackage** and consume it in your app — e.g. `npm pack` the modified package and `npm install` the tarball, or publish it under a private/different package name and depend on that. ⚠️ A plain Git dependency or npm `overrides` / `resolutions` pointing at the repo **won't work**: a Git dependency resolves to the repository's root package, not `packages/@openmaic/<name>`.\n3. Keep your fork's diff small and track upstream — the SDK is actively versioned.\n\nFile v0.3.10:references/extend.md\n\n# Extend Or Build On OpenMAIC (二次开发)\n\n## Charter\n\nSecondary development is a confirmation-heavy, **read-before-modify** guidance flow — not a generation flow. Help the user understand the existing code first, then make targeted changes. Default to **not** editing source under `packages/@openmaic/*`; consume those packages as-is. (Modifying the SDK itself is a different, heavier path — see the last section of [extend-sdk.md](extend-sdk.md).)\n\nThis reference takes priority over the `accessCode` auto-shortcut in Phase 0: if the user's intent is to extend / build on / customize OpenMAIC or consume the `@openmaic/*` SDK, enter this flow **even when a stored `accessCode` exists**. A returning Live Demo user who now wants to do 二开 should be routed here, not silently sent back to Live Demo.\n\n## Secondary-Development Rules\n\n1. **Read before edit.** Before changing any file, read it (and the symbols it imports) so the edit matches surrounding conventions. Do not paste large code blocks into chat — point the user at `file:line` entry points and let them read.\n2. **Toolchain is hard-required.** `pnpm@10.28.0` (root `packageManager`), Node `>=22.19.0` (`.nvmrc` pins `22`). Mismatched pnpm will fail install.\n3. **Forking and disabling CI are conditional, not defaults.** Decide per the user's intent — see Development Environment below — instead of reflexively forking every user.\n\n## Development Environment (Same As Local Deployment)\n\n二开的开发环境本质上就是 OpenMAIC **本地部署环境**——同一套工具链、同一个仓库、同一次 `pnpm install`、同一套 provider key 和启动方式。所以**环境搭建不要在这里另搞一套**：走标准本地部署流程拿到一个能跑的实例，二开只在其上加几个增量。\n\n**在哪里拿代码（按需选，不强制 fork）：**\n\n- **自用 / 不需要远程**：直接 `git clone` 上游 `THU-MAIC/OpenMAIC`，本地改、本地跑。最简单——不 fork、不管 CI。你对上游无写权限，不可能误推；建议本地 `git commit` 到一条分支做版本回滚。\n- **需要远程**（备份 / 多机同步 / 协作 / 从 GitHub 部署 / 回馈上游）：fork → clone 你的 fork → 推到 fork。fork 的唯一意义是\"拥有一个能 push 的远程\"。\n\n**安装与启动** → 复用现有本地部署 reference，不要重写流程：[clone.md](clone.md)（clone + `pnpm install`，后者会在 postinstall 构建全部 `@openmaic/*` 包并同步 vendor 包）、[startup-modes.md](startup-modes.md)（启动方式）、[provider-keys.md](provider-keys.md)（provider key）。\n\n**禁用 publish CI —— 注意\"触发 workflow\"≠\"跑 publish job\"：** fork 自带 `.github/workflows/publish-packages.yml` 和 `publish-openmaic-skill.yml`，要分两层看：\n\n- **触发层（workflow 什么时候跑）**：`publish-packages.yml` 只在 **push 到 main 且改了 `packages/@openmaic/*/package.json`** 时触发（PR 根本不触发这个 workflow）；`publish-openmaic-skill.yml` 在 **PR 或 push 到 main 且动了 `skills/openmaic/**`** 时触发。\n- **job 层（哪些 job 会跑）**：`publish-openmaic-skill.yml` 在 **PR 上只跑** bash-3 兼容性 + preview（dry-run）job——这两个不需要 token；**带 `CLAWHUB_TOKEN` 的 publish job 只在 push（或 main 上的手动非 dry-run dispatch）时运行**。`publish-packages.yml` 的带 `NPM_TOKEN` 的 publish job 同样只在 push 时跑。\n\n所以 fork 里的 token 红叉**只来自命中触发条件的 push**，PR 不会产生；普通 feature 分支推送不匹配触发条件则整个 workflow 都不跑。是否禁用取决于你的 fork 工作流：会往 main 推命中触发的改动就禁用（或去掉触发），否则不用管；纯本地自用、从不 push 同样无需处理。误发版本身已被 environment + token 闸门挡死，不用担心。\n\n**改完代码后运行 / 验证：** 启动方式同 [startup-modes.md](startup-modes.md)，key 同 [provider-keys.md](provider-keys.md)，用 `GET {url}/api/health` 验证，UI / 路由改动在浏览器确认。若你改动了 `packages/@openmaic/*` 的**源码**，要先重建对应包的 `dist/`（消费方解析的是 `dist/` 不是 `src/`；依赖顺序 `dsl → generation → storage → importer → renderer → editor`，`pnpm install` 的 postinstall 已按此顺序构建，单包可用各自 `pnpm run build`）。\n\n## Route To The Right Sub-Reference\n\nAsk the user which of these they want. When unsure, offer 2–3 examples (below) and let them self-identify before loading anything.\n\n- **Customize the OpenMAIC product itself** (change a feature / UI / swap in your own provider, storage, or theme / add a page or API route / embed the renderer or editor inside the product) → Load [extend-cookbook.md](extend-cookbook.md).\n- **Use the `@openmaic/*` SDK to build a new, standalone app** (outside this repo — `npm install @openmaic/renderer` into your own project, not a monorepo workspace) → Load [extend-sdk.md](extend-sdk.md).\n\nTypical examples to help the user pick:\n\n- \"I want OpenMAIC to call my company's LLM / write to my S3 bucket / show my branding\" → **cookbook** (you are modifying the product).\n- \"I want to render OpenMAIC slides, or embed the slide editor, inside my own separate app\" → **SDK** (you are consuming packages, not editing the product).\n- \"I want to add a feature to OpenMAIC and ship it in the product\" → **cookbook** (modifying the product).\n\n## General Gotchas\n\nEach is tagged with where it bites: **[product/fork]** (working inside the OpenMAIC repo), **[SDK]** (consuming packages in a separate app), or **[both]**.\n\n- **[product/fork] `dist/` is gitignored in a source checkout.** In a fork/monorepo, each `@openmaic/*` package ships source only; its `dist/` is produced by `postinstall` (builds `dsl → generation → storage → importer → renderer → editor` in dependency order). If you `git clean -fdx` or nuke `node_modules`, re-run `pnpm install` or the packages won't resolve. (Published npm packages — the [SDK] path — ship built `dist/`, so this does not apply there.)\n- **[product/fork] The vendor bundle is asserted before build.** `pnpm build` runs `node scripts/assert-vendor-maic-importer.mjs && next build`. That guard `stat()`s `public/vendor/maic-importer/index.js`; if missing/empty it exits 1 with an actionable message. `postinstall`'s `sync-maic-importer.mjs` step populates it — re-run `pnpm run sync:maic-importer` if you cleared it.\n- **[product/fork] `workspace:*` are symlinks.** Inside the monorepo, `@openmaic/*` resolve to live `packages/@openmaic/*` source via pnpm workspace links. Editing a package's `src/` is picked up on its next build — but consumers see the built `dist/`, not `src/`, so rebuild the package after source changes.\n- **[both] The renderer hard-depends on Tailwind v4.** `@openmaic/renderer` has `tailwindcss: \">=4\"` as a peer. Tailwind v4 uses `@theme`/`@source` CSS directives, not a JS config — see [extend-cookbook.md](extend-cookbook.md) (branding) before touching styles.\n- **[product/fork] `@/*` path alias** is anchored at the repo root (`tsconfig.json` → `\"@/*\": [\"./*\"]`), not inside `packages/`.\n\nFile v0.3.10:references/generate-flow.md\n\n# Generate Flow\n\n## Preconditions\n\n- Repo path is confirmed\n- Startup mode has been chosen\n- OpenMAIC is healthy at the selected `url`\n- Provider keys are configured\n\n> **Live Demo mode**: If using the OpenMAIC Live Demo (open.maic.chat), all\n> preconditions (repo, startup, provider keys) are already satisfied.\n> Include `Authorization: Bearer <access-code>` header on all requests below.\n> See [live-demo.md](live-demo.md) for details.\n\n## Request Contract\n\n`POST {url}/api/generate-classroom` accepts exactly two fields:\n\n- `requirement` (string, required) — what the classroom should teach. The course language follows the requirement (and any uploaded material); there is no `language` field.\n- `materialIds` (string array, optional) — up to 5 ids returned by `POST {url}/api/materials`, used as source documents in the order given.\n\nNothing else is a request field. Web search, image generation, video generation and TTS narration are attempted automatically whenever their slot (`webSearch`, `image`, `video`, `tts`) resolves to a provider in the server's model configuration for the caller's workspace; they cannot be switched on or off per request, and requests never carry provider choices or API keys. Other fields are ignored, except `pdfContent`, which is rejected with `400 INVALID_REQUEST` (upload the document instead, see below).\n\nDo not rely on request-time model or provider override parameters. To change what a generation job can do, change the slots in `openmaic.yml` or in the model settings (a slot set to `null` is off).\n\n## Keep One Owner Across Requests\n\nUploaded materials belong to the owner the server resolves for the upload request, and `materialIds` only resolve for that same owner.\n\n- `GET {url}/api/generate-classroom/capabilities` needs no owner.\n- If the server resolves a fixed owner — a shared team owner (`PERSISTENCE_SHARED_OWNER_ID` together with `ACCESS_CODE`), single-user mode (`OWNER_SINGLE_USER=true`), or a host that resolves the owner from a credential you send on every request — every request is the same owner automatically.\n- Otherwise the server identifies callers by an anonymous owner cookie that it sets on the first owner-scoped response (including error responses). Reuse one cookie jar on every request of the flow — uploads, the submission, polls and deletions — for example `curl -c cookies.txt -b cookies.txt` on all calls. An upload made without the cookie belongs to a different owner, and its id is unavailable to the submission.\n\nThe job and the classroom it produces belong to the owner that submitted it:\n\n- A job can only be polled by that owner. A poll without the submission's cookie (or credential) is a different owner, and gets the same `404` as an unknown job.\n- The finished classroom is saved in that owner's course library. With a fixed owner it is editable wherever that owner signs in. With an anonymous owner cookie, the classroom opens read-only for anyone who has its URL but cannot be edited from a user's browser, because the browser is a different anonymous owner. If the user wants to edit classrooms generated through the API, the server needs a fixed owner or host authentication.\n\n## Optional: Check Capabilities\n\nTo tell the user in advance what the job is configured to attempt, or which files it can generate from, query:\n\n```text\nGET {url}/api/generate-classroom/capabilities\n```\n\n```json\n{\n  \"success\": true,\n  \"capabilities\": {\n    \"webSearch\": true,\n    \"imageGeneration\": false,\n    \"videoGeneration\": false,\n    \"tts\": true\n  },\n  \"materials\": {\n    \"formats\": [{ \"id\": \"pdf\", \"mime\": \"application/pdf\", \"extensions\": [\".pdf\"] }],\n    \"maxCount\": 5,\n    \"maxTotalBytes\": 157286400,\n    \"maxDocumentBytes\": 52428800,\n    \"maxMediaBytes\": 52428800\n  }\n}\n```\n\n`capabilities` says which optional features the server has a provider configured for; nothing needs to be sent back. It is best-effort, not a promise: if a configured provider fails at run time, the job continues and completes without that output. Today only narration reports this — through `result.ttsCoverage` and `result.warning` — so a missing image, video or search context is not flagged in the result.\n\n`materials.formats` lists the upload types this server can extract text from with its current configuration (plain text, Markdown and PDF always; Office documents, images, audio and video only when a matching extraction service or local media pipeline is configured). Classroom generation uses only the extracted text of each material — no images, slides-as-pictures or video keyframes. With the local media pipeline (ffmpeg) but no server ASR provider, video is listed and audio is not: a video without an audio track extracts, but contributes almost nothing (its text is just \"No audio track\"), and a video with an audio track fails when the job runs, because its speech cannot be transcribed. `maxCount` and `maxTotalBytes` bound one request's `materialIds`; the byte limits apply per file (`maxMediaBytes` for audio/video, `maxDocumentBytes` for everything else). `POST /api/materials` may accept more types than are listed here, but a submission with a material of an unlisted type is refused.\n\n## Requirement-Only Generation\n\nIf the user has already clearly asked to generate the classroom and the preconditions are satisfied, submit the generation job immediately. Do not ask for a second confirmation just before calling `/api/generate-classroom`.\n\n```text\nPOST {url}/api/generate-classroom\n```\n\n```json\n{\n  \"requirement\": \"Create an introductory classroom on quantum mechanics for high school students\"\n}\n```\n\nTreat the `POST` response as job submission only. Expect fields such as:\n\n```json\n{\n  \"success\": true,\n  \"jobId\": \"abc123\",\n  \"status\": \"queued\",\n  \"step\": \"queued\",\n  \"pollUrl\": \"http://localhost:3000/api/generate-classroom/abc123\",\n  \"pollIntervalMs\": 5000\n}\n```\n\n## Generation From Local Files\n\nUse this when the user wants the classroom built from their own files. Check `materials.formats` from the capabilities endpoint first when the file is not plain text, Markdown or PDF.\n\n1. Resolve the absolute path of each file.\n2. Confirm before reading the files.\n3. Upload each file as the raw request body (not multipart):\n\n```text\nPOST {url}/api/materials\nContent-Type: <the file's MIME type, e.g. application/pdf>\nX-Material-Filename: <the file name, percent-encoded if it is not ASCII>\n<raw file bytes>\n```\n\nFor example, with curl:\n\n```bash\ncurl -sS -c cookies.txt -b cookies.txt \\\n  -H 'Content-Type: application/pdf' \\\n  -H 'X-Material-Filename: lecture-notes.pdf' \\\n  --data-binary @/path/to/lecture-notes.pdf \\\n  {url}/api/materials\n```\n\nA successful upload answers `201` with `{ \"materialId\": \"...\", \"originalName\": \"...\", \"bytes\": ..., \"mime\": \"...\", \"extraction\": { \"status\": \"idle\" } }`. Other answers: `413` (the file exceeds the limit; the body's `maxBytes` gives it), `415` (unsupported type), `429` (the owner's material library is full — it holds a bounded number of files and bytes per owner, 100 files and 2 GiB by default; delete materials you no longer need, see below). Extraction happens later, inside the generation job, so `status: \"idle\"` is expected.\n\n4. Submit the job with the returned ids, in the order the documents should be read:\n\n```json\n{\n  \"requirement\": \"Create a classroom from these lecture notes\",\n  \"materialIds\": [\"mat_01...\", \"mat_02...\"]\n}\n```\n\nThe submission is checked before a job is created, and answers `400 INVALID_REQUEST` when:\n\n- an id is unknown, not fully uploaded, deleted, or belongs to someone else (`One or more materials are unavailable`, the same answer for all of these);\n- a material's type has no extractor available on this server;\n- the materials together exceed `maxTotalBytes`.\n\nIf a document still cannot be extracted when the job runs (for example the extraction service fails, or the file contains no text), the job fails rather than generating without it; surface the error to the user.\n\n5. After the job reaches `succeeded` or `failed`, delete the uploads you no longer need:\n\n```text\nDELETE {url}/api/materials/{materialId}\n```\n\nIt answers `200` with `{ \"materialId\": \"...\", \"deleted\": true }`, or a plain `404` for an id the owner does not have — including one already deleted, so a `404` after an earlier successful delete just means it is gone. Do not delete before the job is finished: the job reads the files when it runs, and a job whose material was deleted fails. Deleting frees the owner's library quota; with a shared team owner every caller shares that one quota, so cleaning up matters.\n\n### URLs Are Not Accepted\n\nThere is no way to pass a document URL. The server intentionally never fetches caller-supplied URLs; download the file locally (with the user's confirmation) and upload its bytes instead.\n\n## Polling Loop\n\nAfter the job is submitted:\n\n1. Save `jobId`, `pollUrl`, and `pollIntervalMs`.\n2. Do not submit another generation job while this one is still `queued` or `running`.\n3. Poll:\n\n```text\nGET {pollUrl}\n```\n\n4. Prefer a conservative polling cadence of about 60 seconds between polls for classroom generation jobs, even if `pollIntervalMs` is shorter.\n5. Treat `queued` and `running` as in-progress states.\n6. Stop only when `status` becomes `succeeded` or `failed`.\n\n### Reliability Rules\n\n- Never restart the job just because a poll request fails once.\n- If a poll request returns a transient network error or `5xx`, wait about 60 seconds and retry the same `pollUrl`.\n- Treat a `404` on the `pollUrl` as terminal: the server does not know that job for this owner. Check that the poll carries the submission's cookie (or credential); if it does, stop polling, report the `jobId` to the user, and do not resubmit without their confirmation.\n- If the job is still running after many polls, tell the user it is still in progress and continue polling instead of resubmitting.\n- Prefer fewer poll attempts over aggressive polling. Long-running jobs are more likely to survive agent-loop limits if the tool-call cadence stays low.\n- Within a single agent turn, cap active polling to about 10 minutes. If the job is still not finished, tell the user it is still running and include the `jobId` and `pollUrl` so a later turn can continue checking without resubmitting.\n- Report progress to the user only when `status`, `step`, or visible progress meaningfully changes. Do not spam every poll result.\n- Do not try to recover from auth, provider, model, or base URL errors by changing request parameters. Tell the user to fix OpenMAIC server-side config and retry only after they confirm.\n- On `failed`, surface the server error and include the `jobId`.\n- On `succeeded`, read `result.classroomId` and `result.url` from the final poll response, and also read `result.warning` and `result.ttsCoverage` before telling the user the classroom is ready.\n  - If `result.warning` is set, quote it in the same update and describe narration as incomplete. A warning that says the asset storage is full names the outputs (images, video, narration) the server stopped storing; tell the user those were left out of the classroom.\n  - If `result.ttsCoverage` is set and `written` is less than `total`, tell the user how many narration clips were written and how many speech actions were left silent. The classroom URL is still usable, and narration is incomplete.\n  - A missing `ttsCoverage` means the server has no TTS provider configured, so no narration was generated. A TTS run includes `ttsCoverage`. `warning` is set when `written` is less than `total`, or when the TTS phase failed. A run with no narratable speech (`written: 0`, `total: 0`) has coverage and no `warning`.\n\n## If The Loop Ends First\n\nIf the job is still running when you stop active polling for this turn, tell the user that the classroom generation is still running in the background and invite them to come back a little later to continue checking the same job.\n\nUse natural phrasing such as:\n\n```text\nThe classroom generation is still running in the background.\nJob ID: abc123\n\nCheck back with me in a little while and I can continue tracking this same job without starting over.\n```\n\n## What To Return\n\nReturn the generated classroom ID plus a directly clickable classroom URL.\n\nWhen the succeeded job includes `result.warning` or an incomplete `result.ttsCoverage` (`written` < `total`), say that narration is incomplete in the same reply, quoting `result.warning` when it is present, and still include the classroom ID and URL.\n\nOutput the URL as a raw absolute URL on its own line.\n\nDo not wrap the URL in:\n\n- bold markers such as `**...**`\n- markdown links such as `[title](url)`\n- code formatting such as `` `...` ``\n- angle brackets such as `<...>`\n- markdown tables\n\nUse a compact format like:\n\n```text\nClassroom ID: Uyh82Y32ZK\nClassroom URL:\nhttp://localhost:3001/classroom/Uyh82Y32ZK\n```\n\nIf the job fails, return the job ID plus the server error.\n\nIf generation fails, surface the server error directly instead of paraphrasing it away.\n\nIf the error suggests a provider or model configuration problem, explicitly tell the user to update `openmaic.yml` (with the key in `.env.local`) or the model settings in the web app instead of attempting a runtime override. See [provider-keys.md](provider-keys.md#recognizing-configuration-errors) for the common messages.\n\n## Confirmation Requirements\n\n- Ask before reading local files for upload.\n- Do not ask for a second confirmation before the generation request if the user has already clearly asked you to generate the classroom.\n\nFile v0.3.10:references/live-demo.md\n\n# Live Demo Mode\n\nThe OpenMAIC Live Demo (open.maic.chat) is the cloud edition — the version officially deployed and hosted by the OpenMAIC team, so no local setup is required. Use this when the user has an access code from open.maic.chat and wants to skip local setup.\n\n## Access Code Setup\n\n1. Read `accessCode` from skill config (`~/.openclaw/openclaw.json` → `skills.entries.openmaic.config.accessCode`).\n2. If found, use it directly. Do not ask the user to paste the code into chat.\n3. If not found, tell the user how to get an access code and where to put it:\n   - Get your access code: sign in at https://open.maic.chat, click your account in the top-right corner, open \"访问码设置\" (access code settings), and generate a code (starts with `sk-`).\n   - Add it to the config file: edit `~/.openclaw/openclaw.json` and set `skills.entries.openmaic.config.accessCode` to your access code.\n   Wait for the user to confirm before continuing. Do not ask them to paste the code in chat.\n4. Verify connectivity: `GET https://open.maic.chat/api/health` with `Authorization: Bearer <access-code>`\n   - On success: confirm connection and proceed to generation.\n   - On failure (401): access code is invalid, ask the user to check or regenerate at open.maic.chat and update the config file.\n   - On failure (network): suggest checking network or trying local mode.\n\n## Generating a Classroom\n\nFollow the same generation flow as [generate-flow.md](generate-flow.md) with these differences:\n\n- **Base URL**: `https://open.maic.chat` (hardcoded, not configurable)\n- **Authorization**: Include header `Authorization: Bearer <access-code>` on all API requests\n- **Classroom URL**: `https://open.maic.chat/classroom/{id}`\n\n### Capabilities in Live Demo Mode\n\nOptional features (web search, image and video generation, TTS) follow the Live Demo server's configuration; there are no request flags for them. To see which features a job is configured to attempt and which file types it can use, query `GET /api/generate-classroom/capabilities` on the Live Demo base URL (with the auth header). Upload local files with `POST /api/materials` and pass the returned ids as `materialIds`, then delete them after the job finishes, exactly as in [generate-flow.md](generate-flow.md). Send the same `Authorization` header on every request of the flow (uploads, submission, polls, deletions). The Live Demo instance may update on a different schedule than the local codebase: if the capabilities endpoint answers `404`, the instance predates this contract.\n\n## Quota\n\n- 10 generations per day, independent of web UI quota\n- If generation returns 403 with `Daily quota exhausted`, inform the user of the daily limit and that it resets at midnight.\n\n## Error Handling\n\n| HTTP Status | Meaning | Action |\n|-------------|---------|--------|\n| 401 | Invalid access code | Ask user to check their code or generate a new one at open.maic.chat |\n| 403 | Quota exhausted | Inform daily limit (10), suggest trying tomorrow |\n| 500 | Server error | Suggest retrying later or switching to local mode |\n\nFile v0.3.10:references/provider-keys.md\n\n# Provider Keys\n\n## Critical Boundary\n\nOpenMAIC generation does not automatically reuse the OpenClaw agent's current model or API key.\n\nOpenMAIC resolves every model and key on the server, from its own model configuration:\n\n- `openmaic.yml` (written by the operator; path overridable with `OPENMAIC_CONFIG`) declares providers and assigns models to capability slots. Keys stay in `.env.local` and are referenced from the file as `${VAR}`.\n- The model settings in the OpenMAIC web app (**Settings → Token Plan**, **Model Services** and **Course Model Config**) edit the slots and providers `openmaic.yml` leaves open, for the current workspace.\n\nThis skill does not rely on runtime overrides for model, provider, API key, base URL, or provider type. The old request headers (`x-model`, `x-api-key`, `x-base-url`, `x-model-routes`, `x-*-provider`, …) are deprecated and ignored once a slot is configured; never use them as a workaround.\n\nIf the user wants to change the model or provider, they edit `openmaic.yml` (and `.env.local` for the key) or use the model settings in the web app.\n\n## Interaction Flow\n\n1. Recommend one provider path first (see \"Recommendation Paths\" below). Do not start by asking for an API key.\n2. Ask whether the user wants to configure it in `openmaic.yml` + `.env.local` (recommended for self-hosting and anything reproducible) or in the web app's model settings after starting (simplest for a personal install).\n3. Tell the user exactly which file and fields to edit — they edit the files themselves. Do not offer to write the key for them, do not ask for the literal key in chat, and do not suggest temporary request-time overrides.\n4. Wait for the user to confirm they finished editing before continuing. `openmaic.yml` is read at startup: a running server must be restarted after it changes.\n5. If startup or generation later fails because of auth, provider, or model selection, direct the user back to the same configuration and wait for confirmation before retrying.\n\n## The Configuration File\n\nStart from the example in the repository:\n\n```bash\ncp .env.example .env.local\ncp openmaic.example.yml openmaic.yml\n```\n\nAs shipped, the example has one active provider (`openai`, reading `OPENAI_API_KEY`) and the `llm` slot; everything else is commented out. Startup refuses any `${VAR}` that is not set, so tell the user to either set that one key or replace the provider with the path they chose below, and to uncomment optional blocks only together with the variables they name.\n\nThree concepts:\n\n- **Provider** — an account the server can call: an id the user chooses, a `preset` (which vendor), and `apiKey: ${VAR}`.\n- **Slot** — a use of AI. `llm` is the default chat model for everything; `course.outline`, `course.content`, `course.content.slide`, `classroom`, `agent`, … override it for one use; `tts`, `asr`, `image`, `video`, `webSearch` and `document` are the media and tool capabilities.\n- **Assignment** — `slot: <provider id>:<model id>`, or `<provider id>` alone for search/document/media providers (their default model), or `null` to turn the capability off.\n\nMinimal file:\n\n```yaml\nproviders:\n  anthropic:\n    preset: anthropic\n    apiKey: ${ANTHROPIC_API_KEY}\n\nslots:\n  llm: anthropic:claude-sonnet-4-6\n```\n\nwith `ANTHROPIC_API_KEY=sk-ant-...` in `.env.local`.\n\nSlots written in `openmaic.yml` are locked for the web UI; slots left out follow their parent (`llm` for chat slots) and can be changed in the model settings.\n\n## Recommendation Paths\n\n### 1. One Key for Everything (token plan)\n\nRecommended when the user wants illustrations, narration, video and web search with the least setup. A token plan preset covers several capabilities with one key:\n\n```yaml\nproviders:\n  minimax:\n    preset: minimax\n    apiKey: ${MINIMAX_API_KEY}\n\nslots:\n  llm: minimax:MiniMax-M3\n  image: minimax\n  video: minimax\n  tts: minimax\n  webSearch: minimax\n```\n\nOther token plan presets: `tokendance`, `volcengine-ark`, `kimi-coding-plan` (chat only).\n\n### 2. A Single Chat Vendor\n\nRecommended when the user already has a key for one vendor:\n\n```yaml\nproviders:\n  google:\n    preset: google\n    apiKey: ${GOOGLE_API_KEY}\n\nslots:\n  llm: google:gemini-2.5-flash\n```\n\nSame shape for `openai` (`OPENAI_API_KEY`), `anthropic`, `deepseek`, `qwen`, `glm`, `kimi`, `openrouter` and the other chat presets.\n\n### 3. Web App Only\n\nFor a personal install where the user does not want to edit files: start OpenMAIC, open **Settings → Token Plan** and enter a plan key, or **Settings → Model Services** and enter a service key. The key is entered in the browser, stored encrypted on the server, and never shown again. Nothing to edit in `openmaic.yml`.\n\n### 4. Existing Environment-Variable Setups\n\nA deployment configured through provider variables (`OPENAI_API_KEY`, …), `server-providers.yml`, `DEFAULT_MODEL` and `MODEL_FALLBACK` still works while there is no `openmaic.yml`; the server translates it at startup and logs a deprecation notice. Recommend moving to `openmaic.yml` when the user touches the configuration anyway. `MODEL_ROUTES` is no longer read: a server that sets it without `openmaic.yml` refuses to start, and the per-stage models must be written as slots (see the Configuration docs, \"Migrating from the legacy configuration\").\n\n## Model Reference Rule\n\nIn `openmaic.yml`, a chat slot always names `<provider id>:<model id>`, where the provider id is the key under `providers` (not necessarily the preset):\n\n- `google:gemini-2.5-flash`\n- `anthropic:claude-sonnet-4-6`\n- `openai:gpt-5.4-mini`\n- `deepseek:deepseek-v4-flash`\n\nA chat slot with a provider id alone (`llm: openai`) is refused at startup. Non-chat slots may use the provider id alone.\n\nThe exact model IDs above are examples. Model names change as providers release new versions — if a model ID is rejected by the provider, direct the user to the provider's official docs (or the Supported models page) for the current name.\n\n## Optional Features\n\nThese features need their own slot assigned, usually with a provider of their own. Ask the user if they want any of them after the chat model works. Each provider is declared once under `providers` and assigned to its slot.\n\n| Feature | Slot | Example presets |\n|---------|------|-----------------|\n| Web Search | `webSearch` | `tavily`, `exa`, `bocha`, `brave`, `baidu` |\n| Image Generation | `image` | `seedream`, `qwen-image`, `nano-banana`, `openai-image` |\n| Video Generation | `video` | `seedance`, `kling`, `veo`, `minimax-video` |\n| TTS | `tts` | `openai-tts`, `azure-tts`, `glm-tts`, `qwen-tts`, `minimax-tts` |\n| Speech Recognition | `asr` | `openai-whisper`, `qwen-asr`, `funasr-asr` |\n| Document Parsing | `document` | `mineru-cloud`, `mineru`, `alidocmind` |\n\nExample:\n\n```yaml\nproviders:\n  tavily:\n    preset: tavily\n    apiKey: ${TAVILY_API_KEY}\n  seedream:\n    preset: seedream\n    apiKey: ${IMAGE_SEEDREAM_API_KEY}\n\nslots:\n  webSearch: tavily\n  image: seedream\n```\n\nThese are all optional. Classroom generation works without them — they only unlock richer content. To turn one off explicitly, set its slot to `null`.\n\n## Recognizing Configuration Errors\n\n- **Server exits at startup with `Invalid model configuration in …/openmaic.yml`** — the message lists each problem with its path (an unset `${VAR}`, an unknown preset or slot, a provider not declared under `providers`, a chat slot without a model id). Relay it to the user unchanged.\n- **`MODEL_ROUTES does not carry over to the model configuration …`** — the user must write the per-stage models as slots in `openmaic.yml` and remove `MODEL_ROUTES`.\n- **`No model is configured for <slot>`** — assign that slot (or its parent, e.g. `llm`) in `openmaic.yml` or the model settings.\n- **`The <slot> capability is turned off`** — the slot is `null`; the operator turned it off on purpose.\n\n## Recommended Prompts To The User\n\nExample phrasing the agent can adapt:\n\n- \"I recommend configuring OpenMAIC with `openmaic.yml`: copy `openmaic.example.yml`, keep your key in `.env.local`, and tell me when you're done.\"\n- \"For the least setup, one token plan key covers chat, images, narration, video and search. If you already have an Anthropic or Google key, a single `llm` line is enough. Which path do you want?\"\n\nThe \"do not ask for the key in chat / do not offer to write it\" rules are covered in [Interaction Flow](#interaction-flow) above — do not open by requesting the key.\n\nFile v0.3.10:references/startup-modes.md\n\n# Startup Modes\n\n## Goal\n\nHelp the user choose how OpenMAIC should run before you start anything.\n\n## Options\n\n### 1. Development Mode\n\nRecommended for first-time setup and debugging. Courses are stored in PostgreSQL and the server refuses to start without `DATABASE_URL`, so start the local development database first (a separate PostgreSQL in Docker on `127.0.0.1:5432`) and uncomment the local `DATABASE_URL` line in `.env.local`:\n\n```bash\npnpm db:up\npnpm dev\n```\n\nTradeoff:\n\n- Fastest feedback loop\n- Best for validating config changes\n- Not representative of production startup\n\n### 2. Production-Like Local Mode\n\nRecommended when the user wants behavior closer to a deployed server. Needs `DATABASE_URL` too (`pnpm db:up` locally).\n\n```bash\npnpm build && pnpm start\n```\n\nTradeoff:\n\n- Closer to production\n- Slower startup than `pnpm dev`\n\n### 3. Docker Compose\n\nUse only when the user explicitly wants containerized startup or wants to avoid local Node setup details.\n\n```bash\ndocker compose up --build\n```\n\nThis starts the app and PostgreSQL (courses are stored server-side) in single-user mode, published on `127.0.0.1:3000` only. To reach it from other machines, start with `OPENMAIC_PUBLISH_ADDRESS=0.0.0.0` and set `ACCESS_CODE` in `.env.local` first; without an access code anyone who can reach it shares, edits and can delete the single library (the server warns at startup but still runs).\n\nTradeoff:\n\n- Cleaner isolation\n- Heavier and slower\n- Harder to debug application-level issues quickly\n\n## Recommendation Order\n\n1. `pnpm dev`\n2. `pnpm build && pnpm start`\n3. `docker compose up --build`\n\n## Health Check\n\nAfter startup, verify:\n\n```bash\ncurl -fsS http://localhost:3000/api/health\n```\n\nIf the skill config provides a custom `url`, use that instead.\n\n## Confirmation Requirements\n\n- Ask the user to choose one startup mode.\n- Ask again before running the selected command.\n\nFile v0.3.10:skill-card.md\n\n## Description:\n\nGuides users through OpenMAIC setup, interactive classroom generation, and product or SDK extension.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[wyuc](https://clawhub.ai/user/wyuc)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and educators use this skill to set up OpenMAIC locally or use its hosted service, generate interactive classrooms, and customize the product or build with its SDK.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Live Demo sends the configured access code to the hosted OpenMAIC service.\n\nMitigation: Use the hosted service only when intended; keep the code in local configuration rather than sharing it in chat.\n\nRisk: Approved local teaching materials may be uploaded to the selected OpenMAIC server.\n\nMitigation: Confirm before reading and uploading files, and review their contents and destination first.\n\nRisk: Cloning, installing dependencies, or starting Docker changes the local environment.\n\nMitigation: Review the commands and obtain confirmation before running them.\n\n## Reference(s):\n\n- [OpenMAIC on ClawHub](https://clawhub.ai/wyuc/skills/openmaic)\n- [OpenMAIC hosted service](https://open.maic.chat)\n- [Live Demo guide](references/live-demo.md)\n- [Classroom generation guide](references/generate-flow.md)\n- [Provider configuration guide](references/provider-keys.md)\n- [Extension guide](references/extend.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration instructions, Code, Classroom links]\n\n**Output Format:** [Markdown with code blocks and links]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May provide a generated classroom URL after a successful job.]\n\n## Skill Version(s):\n\n0.3.10 (source: server-resolved ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.3.9: 11 files, 23339 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7522b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (7630b), references/live-demo.md (2742b), references/provider-keys.md (5385b), references/startup-modes.md (1893b), skill-card.md (2186b), SKILL.md (6324b), _meta.json (127b)\n\nFile v0.3.9:SKILL.md\n\n---\nname: openmaic\ndescription: OpenMAIC assistant for setting up, generating, and extending OpenMAIC. Use when the user wants to use OpenMAIC, generate a multi-agent interactive classroom, or build on / extend / customize OpenMAIC and its @openmaic/* SDK (secondary development, 二开) — covers Live Demo or local setup, startup modes, provider keys, classroom generation, and secondary development (forking, providers/storage/themes, routes, or the renderer/editor).\nuser-invocable: true\nmetadata: { \"openclaw\": { \"emoji\": \"🏫\" } }\n---\n\n# OpenMAIC Skill\n\nUse this as a guided, confirmation-heavy SOP. Do not compress the whole setup into one reply and do not perform state-changing actions without explicit user confirmation.\n\n## Core Rules\n\n- Move one phase at a time.\n- Before any state-changing action, ask for confirmation.\n- If local state already exists, show what you found and ask whether to keep it.\n- Do not assume the OpenClaw agent's own model or API key will be reused by OpenMAIC.\n- OpenMAIC classroom generation uses OpenMAIC server-side provider config.\n- This skill must not rely on any request-time model or provider overrides.\n- Only OpenMAIC server-side config files may control provider selection and defaults.\n- Do not default to asking the user to paste API keys into chat.\n- Prefer guiding the user to edit local config files themselves.\n- Do not offer to write API keys into config files on the user's behalf.\n- Once setup is complete and the user clearly asks to generate a classroom, do not ask for a second confirmation before submitting the generation job.\n- Keep confirmations for local file reads such as reading a PDF from disk.\n\n## Optional Skill Config\n\nIf present, read defaults from `~/.openclaw/openclaw.json` under:\n\n```jsonc\n{\n  \"skills\": {\n    \"entries\": {\n      \"openmaic\": {\n        \"enabled\": true,\n        \"config\": {\n          \"accessCode\": \"sk-xxx\",\n          \"repoDir\": \"/path/to/OpenMAIC\",\n          \"url\": \"http://localhost:3000\"\n        }\n      }\n    }\n  }\n}\n```\n\n- If `accessCode` is present, default to Live Demo mode and skip the mode-selection prompt — unless the user's intent is to extend/build on OpenMAIC (see the exception in Phase 0).\n- Use `repoDir` and `url` only as defaults for local mode.\n- Still confirm before acting.\n\n## SOP Phases\n\n### 0. Choose Mode\n\nFirst check skill config for `accessCode`. If present, announce that a stored access code was found and proceed directly to Live Demo mode (load [references/live-demo.md](references/live-demo.md), skip phases 1–4). Do not ask the user to paste the code again. **Exception:** if the user's stated intent is to extend / build on / customize OpenMAIC or consume the `@openmaic/*` SDK (二次开发 / 二开 / SDK), do not auto-shortcut — go to the extend branch below regardless of `accessCode`. A returning Live Demo user who now wants to do 二开 should be routed to extend, not silently sent back to Live Demo.\n\nIf no `accessCode` in config (or the extend exception above applies), ask the user how they want to use OpenMAIC:\n\n1. **Use the OpenMAIC Live Demo** (recommended for quick start) — The cloud edition: the version officially deployed and hosted by the OpenMAIC team at open.maic.chat. Requires an access code (starts with `sk-`). Get yours by signing in at https://open.maic.chat, clicking your account in the top-right corner, opening \"访问码设置\" (access code settings), and generating a code; then add it to `~/.openclaw/openclaw.json` under `skills.entries.openmaic.config.accessCode`. No local setup needed.\n2. **Run locally** — Clone the repo, configure provider keys, and run on your machine.\n3. **Extend or build on OpenMAIC (二次开发)** — Fork the repo and customize the product, or consume the `@openmaic/*` SDK to build something new.\n\nIf the user chooses Live Demo mode, load [references/live-demo.md](references/live-demo.md) and skip phases 1–4.\nIf the user chooses local mode, proceed to phase 1 as usual.\nIf the user chooses to extend/build on OpenMAIC, load [references/extend.md](references/extend.md) and skip the setup/generation phases.\n\n### 1. Clone Or Reuse Existing Repo\n\nLoad [references/clone.md](references/clone.md).\n\nUse this when the user has not installed OpenMAIC yet or when you need to confirm which local checkout to use.\n\n### 2. Choose Startup Mode\n\nLoad [references/startup-modes.md](references/startup-modes.md).\n\nUse this after the repo location is confirmed. Present the available startup modes, recommend one, and wait for the user's choice.\n\n### 3. Configure Provider Keys\n\nLoad [references/provider-keys.md](references/provider-keys.md).\n\nUse this before starting classroom generation. Recommend a provider path and tell the user exactly which config file to edit themselves. If generation later fails due to provider/model/auth issues, return to this phase and direct the user to update the same server-side config files.\n\nAfter the core LLM key is configured, ask the user if they want to enable optional features (web search, image generation, video generation, TTS). Each requires its own provider key — see the \"Optional Features\" section in provider-keys.md.\n\n### 4. Start And Verify OpenMAIC\n\nAfter the user has chosen a startup mode and configured keys, start OpenMAIC using the chosen method, then verify the service with `GET {url}/api/health`.\n\n### 5. Generate A Classroom\n\nLoad [references/generate-flow.md](references/generate-flow.md).\n\nUse this only after the service is healthy. Confirm before reading local PDFs. If the user has already clearly asked to generate, do not ask for a second confirmation before submitting the generation job, and then follow the polling loop until it succeeds or fails. Only send the supported content fields for generation requests. For long-running jobs, prefer sparse polling and tell the user to check back later if the turn ends before completion.\n\n## Response Style\n\n- Keep each step short and explicit.\n- Prefer 2-3 concrete options when the user must choose.\n- Always include the recommended option first and explain why in one sentence.\n- After a step completes, say what changed and what the next confirmation is for.\n- When returning a classroom link, place the raw absolute URL on its own line with no bold, markdown link syntax, code formatting, or tables.\n\nFile v0.3.9:_meta.json\n\n{\n  \"ownerId\": \"kn7cjfn5dkygrges9scanxv97d82x3t1\",\n  \"slug\": \"openmaic\",\n  \"version\": \"0.3.9\",\n  \"publishedAt\": 1790675520439\n}\n\nFile v0.3.9:references/clone.md\n\n# Clone Or Reuse Existing Repo\n\n## Goal\n\nEstablish which OpenMAIC checkout will be used for setup and runtime actions.\n\n## Procedure\n\n1. Check whether OpenMAIC already exists locally.\n2. If a checkout exists, show the path and ask whether to reuse it.\n3. If no checkout exists, propose cloning the repo and ask for confirmation.\n4. After clone, confirm dependency installation separately.\n\n## Recommended Path\n\n- Recommended: reuse an existing checkout if it is already on the target branch.\n- Otherwise: clone a fresh checkout from GitHub, then install dependencies.\n\n## Commands\n\nClone:\n\n```bash\ngit clone https://github.com/THU-MAIC/OpenMAIC.git\ncd OpenMAIC\n```\n\nInstall dependencies:\n\n```bash\npnpm install\n```\n\n## Confirmation Requirements\n\n- Ask before `git clone`.\n- Ask before `pnpm install`.\n- If the repo is dirty, tell the user and ask whether to continue with that checkout.\n\nFile v0.3.9:references/extend-cookbook.md\n\n# Extend The OpenMAIC Product (Cookbook)\n\n## Scope\n\nYou are working **inside a fork of the OpenMAIC product** (the Next.js app at the repo root), customizing it in place. If instead you want to consume `@openmaic/*` in a separate app, use [extend-sdk.md](extend-sdk.md) instead.\n\nEntry points below are given as **file + symbol name** (not line numbers — they drift). Read the file, then jump to the symbol. The `@/*` path alias is anchored at the repo root.\n\n## Task 1 — Swap Or Add An AI Provider\n\n**Goal:** route generation to a different provider, or register a brand-new one.\n\nTwo distinct cases:\n\n- **Use an already-supported provider** (it's in the union below): no source change. Configure keys/models in `.env.local` or `server-providers.yml` — follow [provider-keys.md](provider-keys.md). Mind the `DEFAULT_MODEL=provider:model` prefix (without a prefix, parsing defaults to OpenAI).\n- **Register a NEW provider** (source change):\n  1. Add the id to the `BuiltInProviderId` union in `lib/types/provider.ts`.\n  2. Register its config + models in the `PROVIDERS` registry in `lib/ai/providers.ts`.\n  3. **Env wiring — required if you want `.env.local` to work:** add a `PREFIX: 'your-id'` entry to `LLM_ENV_MAP` in `lib/server/provider-config.ts`. That map is what actually reads `<PREFIX>_API_KEY` / `_BASE_URL` / `_MODELS` from env — without an entry, your env vars are **silently ignored**. (Bedrock is special-cased separately via `applyBedrockProviderConfig`.)\n  4. Then set the key in `.env.local`. **Alternatively**, skip step 3 and configure via `server-providers.yml` — its entries are keyed by provider id and do not need an `LLM_ENV_MAP` entry.\n\n**Gotcha:** OpenMAIC has **no hardcoded model fallback**. If `DEFAULT_MODEL` is unset, generation fails rather than picking a default — always set it.\n\n## Task 2 — Server-Side Persistence (PostgreSQL / S3)\n\n**Goal:** understand where documents, runtime state and assets live, and point them at your database.\n\nAccurate topology (there is **no local-file backend**, and no browser-storage mode):\n\n- **Always server-backed:** documents, runtime state and assets are stored on the server. The server **refuses to start without `DATABASE_URL`**; locally, `pnpm db:up` starts a separate development PostgreSQL (its own Compose project and volume) on `127.0.0.1` and `.env.example` carries the matching `DATABASE_URL`.\n- **Client side:** `lib/persistence/bootstrap.ts` configures the browser's HTTP-backed `HttpRuntimeStore` / `HttpDocumentStore` / `HttpAssetStore`, which call `/api/persistence`. Those calls carry no credential of their own: the server attributes them to the owner the owner identity seam (`lib/server/identity/`) resolves, and the runtime learner key is that owner id. Only device-local state (settings, playback position, local media cache) stays in the browser (`lib/device-storage/`).\n- **Server side:** the `/api/persistence` catch-all (`app/api/persistence/[...path]/route.ts`) persists **documents + runtime to PostgreSQL**, and **asset bytes to PostgreSQL or S3**. The byte-layer selection lives in `lib/persistence/asset-byte-store.ts` (`configuredS3Bucket` / `lazyAssetByteStore`) and is strictly three-way: **unset/empty** `ASSET_S3_BUCKET` ⇒ `PgAssetByteStore`; a **valid** bucket ⇒ S3; an **invalid** bucket name ⇒ asset operations **fail** — validation throws, there is no fallback to PG. (The failure isn't cached: the next asset request retries, and only asset traffic is affected — document/runtime requests keep working.)\n- The backends themselves come from `@openmaic/storage` subpaths (`@openmaic/storage/document/pg`, `@openmaic/storage/runtime/pg`, `@openmaic/storage/asset/pg-bytes`, `@openmaic/storage/asset/s3-bytes`) — see the storage table in [extend-sdk.md](extend-sdk.md).\n\n**Gotcha:** S3 additionally needs `@aws-sdk/client-s3` (optional peer of `@openmaic/storage`) installed in the app, and PG needs a reachable Postgres + the package's schema-ensure step. The removed `NEXT_PUBLIC_PERSISTENCE` build switch is ignored.\n\n## Task 3 — Branding / UI / Theme\n\n**Goal:** change title, fonts, color tokens, or preset themes.\n\nEntry points:\n\n- Title + fonts: `app/layout.tsx` (the title metadata; fonts include `@openmaic/renderer/fonts.css`).\n- Design tokens: `app/globals.css` — Tailwind v4 (`@import 'tailwindcss'`, the `@source` directives that scope the renderer's classes, and the `@theme inline { … }` block for custom tokens).\n- Preset themes: the `PRESET_THEMES` array in `configs/theme.ts`.\n\n**Gotcha:** This is Tailwind **v4** — config lives in CSS via `@theme`/`@source`, **not** in `tailwind.config.js`. The renderer's class names are discovered through the `@source` scans of its `dist`; keep token names consistent across `@theme` and the renderer or styles silently drop.\n\n## Task 4 — Add A Page Or API Route\n\n**Goal:** ship a new screen or a new server endpoint.\n\nEntry points:\n\n- Page (App Router): `app/<segment>/page.tsx`.\n- API route: `app/api/<segment>/route.ts`.\n- Reuse the shared server helpers instead of reimplementing: `lib/server/api-response.ts`, `lib/server/llm-error-response.ts`, `lib/server/ssrf-guard.ts`, `lib/server/proxy-fetch.ts`.\n- Import app code via the `@/*` alias (e.g. `import { … } from '@/lib/…'`).\n\n**Gotcha:** Any route that fetches a user-supplied URL must go through `lib/server/ssrf-guard.ts` and `lib/server/proxy-fetch.ts` — do not call `fetch()` directly with attacker-controlled hosts. LLM error responses should go through `llm-error-response.ts` to stay consistent with the rest of the API.\n\n## Task 5 — Embed The Renderer Or Editor\n\n**Goal:** drop a slide canvas (read-only) or the full editable slide surface into a component.\n\nEntry points (real usage examples to copy):\n\n- Render-only `SlideCanvas` from `@openmaic/renderer`: see `components/slide-renderer/SlideThumbnail.tsx`.\n- Editable surface `EditableSlideCanvasWithUI` from `@openmaic/editor/ui`: see `components/edit/surfaces/slide/RendererEditorCanvas.tsx`.\n- Types for slide data: `Slide`, `PPTElement` from `@openmaic/dsl`.\n\n**Gotcha (CSS is mandatory):** The renderer renders unstyled/broken without its fonts and Tailwind classes. In the consuming layout/globals: `@import '@openmaic/renderer/fonts.css';` (or the JS import form), and ensure the renderer's classes are in Tailwind v4's content scan (an `@source` directive pointing at the renderer's `dist`). Note `fonts.css` is **generated** (regen via the renderer's `genfonts` script) and self-hosts CJK faces by fetching woff2 on demand from `https://file.maic.chat/fonts/<name>.woff2` — relevant for offline/custom-font operation. The editor transitively requires the renderer's full peer stack (Tailwind v4, `motion`, optionally `echarts`/`shiki`) — not just `react`/`react-dom`.\n\n## After Your Edits — Run And Verify\n\nThis product still runs like the stock app; don't reinvent the startup steps:\n\n- Dev server / startup mode → [startup-modes.md](startup-modes.md).\n- Provider keys the running server needs → [provider-keys.md](provider-keys.md).\n- Verify with `GET {url}/api/health` (Phase 4), then confirm UI/route changes in the browser.\n\nRebuild sequence when you touch `packages/@openmaic/*` source: consumers resolve to the built `dist/`, not `src/`, so rebuild the changed package (dependency order: `dsl → generation → storage → importer → renderer → editor`). `pnpm install`'s postinstall already does this in order; for a single package use its `pnpm run build`.\n\nFile v0.3.9:references/extend-sdk.md\n\n# Consume The @openmaic/* SDK (Build A New App)\n\n## Scope\n\nYou are building a **separate app** that consumes `@openmaic/*` packages via `npm install` — not working inside the OpenMAIC monorepo. If you are customizing the OpenMAIC product itself, use [extend-cookbook.md](extend-cookbook.md) instead.\n\nThe SDK is early-stage (`0.x`). Versions below are current as of this writing — confirm against the registry when you install.\n\n## Package Quick Reference\n\n| Package | Version | Purpose | Key Exports | Peer Deps |\n|---|---|---|---|---|\n| `@openmaic/dsl` | 0.8.0 | The slide **contract** — types + JSON schema. Zero runtime deps. | `Slide`, `PPTElement`, `./schema/*` | none |\n| `@openmaic/renderer` | 0.1.0 | Render slides to DOM (read-only). | `SlideCanvas`, `./snapshot`→`slideToPng` | react ≥18, react-dom ≥18, motion ≥11, tailwindcss ≥4; **optional:** echarts ≥5, shiki ≥1 |\n| `@openmaic/editor` | 0.0.2 | Editable slide surface + prosemirror editor. | `EditableSlideCanvasWithUI` (from `./ui`) | react ≥18, react-dom ≥18 **+ renderer's full peer stack transitively** |\n| `@openmaic/generation` | 0.3.0 | LLM-driven scene/lesson generation. | `generateSceneContent` | none |\n| `@openmaic/storage` | 0.2.5 | Document / Runtime / Asset / KV stores (Browser · HTTP · PG · S3). | see table below | **optional:** `@aws-sdk/client-s3` |\n| `@openmaic/importer` | 0.1.2 | Import PPTX / PDF into the DSL. | `importPptx` | none |\n\n> `editor` is `0.0.2`. Its own `peerDependencies` list only `react`/`react-dom`, but it `dependencies` on `@openmaic/renderer`, so you must also satisfy the renderer's peers (tailwindcss v4, motion, and optionally echarts/shiki) or it breaks at render time.\n\n## Minimal Starter — Render A Slide\n\n1. Install peers and the package (pin exact versions for `0.x`):\n   ```bash\n   npm i @openmaic/dsl@0.8.0 @openmaic/renderer@0.1.0 \\\n         react@^18 react-dom@^18 motion@^11 tailwindcss@^4\n   # only if you render charts / code-highlighted blocks:\n   npm i echarts@^5 shiki@^1\n   ```\n2. Mandatory CSS — without it the canvas renders unstyled:\n   ```css\n   @import 'tailwindcss';\n   @import '@openmaic/renderer/fonts.css';\n   ```\n   ...and ensure Tailwind v4 scans the renderer's classes, e.g. `@source '../node_modules/@openmaic/renderer/dist';` in your globals. (`fonts.css` is generated and fetches CJK woff2 on demand from `https://file.maic.chat/fonts/<name>.woff2` — relevant for offline/custom-font needs.)\n3. Render:\n   ```tsx\n   import { SlideCanvas } from '@openmaic/renderer';\n   import type { Slide } from '@openmaic/dsl';\n   ```\n   For a working example of props/usage, read `components/slide-renderer/SlideThumbnail.tsx` in the OpenMAIC repo.\n\n## Precise Import Paths\n\n| Want | Import |\n|---|---|\n| Slide / element types | `@openmaic/dsl` (`Slide`, `PPTElement`) |\n| JSON schema (validation) | `@openmaic/dsl/schema/*` |\n| Render a slide | `@openmaic/renderer` (`SlideCanvas`) |\n| Snapshot a slide → PNG | `@openmaic/renderer/snapshot` (`slideToPng`) |\n| Editable slide surface | `@openmaic/editor/ui` (`EditableSlideCanvasWithUI`) |\n| Generate lesson content | `@openmaic/generation` (`generateSceneContent`) |\n| Import a PPTX | `@openmaic/importer` (`importPptx`) |\n| Storage backends | see table below |\n\n## Storage Backends — Exact Subpaths\n\nThere is **no** bare `@openmaic/storage/document`, `/runtime`, or `/asset` subpath — always use the **backend-suffixed** form. The main barrel (`@openmaic/storage`) is asymmetric:\n\n| Domain | Browser | HTTP | PG | S3 |\n|---|---|---|---|---|\n| **Document** | barrel `.` | `./document/http` | `./document/pg` | — |\n| **Runtime** | barrel `.` | `./runtime/http` ⚠️ | `./runtime/pg` ⚠️ | — |\n| **Asset** | barrel `.` | `./asset/http` | `./asset/pg`, `./asset/pg-bytes` | `./asset/s3-bytes` |\n| **KV** | barrel `.` | `./kv/http` | — | — |\n| Server helpers | `./server`, `./server/reference` | | | |\n\n⚠️ **Runtime asymmetry (the main gotcha):** `HttpRuntimeStore` and `PgRuntimeStore` are **only** reachable via `@openmaic/storage/runtime/http` and `@openmaic/storage/runtime/pg` — they are **not** in the main barrel. `BrowserRuntimeStore` is. Document/Asset/KV backends are all in the barrel. Importing `PgRuntimeStore` from `@openmaic/storage` will fail with \"not exported\".\n\nPG backends ship an `ensureSchema` (plus a `*_PG_SCHEMA` constant) you must run once. S3 needs `@aws-sdk/client-s3` installed.\n\n## Version Pinning\n\nAll six packages are `0.x`. Pin **exact** versions (e.g. `\"@openmaic/renderer\": \"0.1.0\"`), not `^` ranges — `0.x` semver treats minor bumps as breaking, and these packages are still moving fast.\n\n## If You Need To Change The SDK Itself\n\nConsuming via `npm install` means you treat the SDK as a black box. To modify SDK behavior, the path is heavier:\n\n1. Fork `THU-MAIC/OpenMAIC`, edit the package source under `packages/@openmaic/*`, rebuild its `dist/`.\n2. Produce an installable artifact for **just that subpackage** and consume it in your app — e.g. `npm pack` the modified package and `npm install` the tarball, or publish it under a private/different package name and depend on that. ⚠️ A plain Git dependency or npm `overrides` / `resolutions` pointing at the repo **won't work**: a Git dependency resolves to the repository's root package, not `packages/@openmaic/<name>`.\n3. Keep your fork's diff small and track upstream — the SDK is actively versioned.\n\nFile v0.3.9:references/extend.md\n\n# Extend Or Build On OpenMAIC (二次开发)\n\n## Charter\n\nSecondary development is a confirmation-heavy, **read-before-modify** guidance flow — not a generation flow. Help the user understand the existing code first, then make targeted changes. Default to **not** editing source under `packages/@openmaic/*`; consume those packages as-is. (Modifying the SDK itself is a different, heavier path — see the last section of [extend-sdk.md](extend-sdk.md).)\n\nThis reference takes priority over the `accessCode` auto-shortcut in Phase 0: if the user's intent is to extend / build on / customize OpenMAIC or consume the `@openmaic/*` SDK, enter this flow **even when a stored `accessCode` exists**. A returning Live Demo user who now wants to do 二开 should be routed here, not silently sent back to Live Demo.\n\n## Secondary-Development Rules\n\n1. **Read before edit.** Before changing any file, read it (and the symbols it imports) so the edit matches surrounding conventions. Do not paste large code blocks into chat — point the user at `file:line` entry points and let them read.\n2. **Toolchain is hard-required.** `pnpm@10.28.0` (root `packageManager`), Node `>=22.19.0` (`.nvmrc` pins `22`). Mismatched pnpm will fail install.\n3. **Forking and disabling CI are conditional, not defaults.** Decide per the user's intent — see Development Environment below — instead of reflexively forking every user.\n\n## Development Environment (Same As Local Deployment)\n\n二开的开发环境本质上就是 OpenMAIC **本地部署环境**——同一套工具链、同一个仓库、同一次 `pnpm install`、同一套 provider key 和启动方式。所以**环境搭建不要在这里另搞一套**：走标准本地部署流程拿到一个能跑的实例，二开只在其上加几个增量。\n\n**在哪里拿代码（按需选，不强制 fork）：**\n\n- **自用 / 不需要远程**：直接 `git clone` 上游 `THU-MAIC/OpenMAIC`，本地改、本地跑。最简单——不 fork、不管 CI。你对上游无写权限，不可能误推；建议本地 `git commit` 到一条分支做版本回滚。\n- **需要远程**（备份 / 多机同步 / 协作 / 从 GitHub 部署 / 回馈上游）：fork → clone 你的 fork → 推到 fork。fork 的唯一意义是\"拥有一个能 push 的远程\"。\n\n**安装与启动** → 复用现有本地部署 reference，不要重写流程：[clone.md](clone.md)（clone + `pnpm install`，后者会在 postinstall 构建全部 `@openmaic/*` 包并同步 vendor 包）、[startup-modes.md](startup-modes.md)（启动方式）、[provider-keys.md](provider-keys.md)（provider key）。\n\n**禁用 publish CI —— 注意\"触发 workflow\"≠\"跑 publish job\"：** fork 自带 `.github/workflows/publish-packages.yml` 和 `publish-openmaic-skill.yml`，要分两层看：\n\n- **触发层（workflow 什么时候跑）**：`publish-packages.yml` 只在 **push 到 main 且改了 `packages/@openmaic/*/package.json`** 时触发（PR 根本不触发这个 workflow）；`publish-openmaic-skill.yml` 在 **PR 或 push 到 main 且动了 `skills/openmaic/**`** 时触发。\n- **job 层（哪些 job 会跑）**：`publish-openmaic-skill.yml` 在 **PR 上只跑** bash-3 兼容性 + preview（dry-run）job——这两个不需要 token；**带 `CLAWHUB_TOKEN` 的 publish job 只在 push（或 main 上的手动非 dry-run dispatch）时运行**。`publish-packages.yml` 的带 `NPM_TOKEN` 的 publish job 同样只在 push 时跑。\n\n所以 fork 里的 token 红叉**只来自命中触发条件的 push**，PR 不会产生；普通 feature 分支推送不匹配触发条件则整个 workflow 都不跑。是否禁用取决于你的 fork 工作流：会往 main 推命中触发的改动就禁用（或去掉触发），否则不用管；纯本地自用、从不 push 同样无需处理。误发版本身已被 environment + token 闸门挡死，不用担心。\n\n**改完代码后运行 / 验证：** 启动方式同 [startup-modes.md](startup-modes.md)，key 同 [provider-keys.md](provider-keys.md)，用 `GET {url}/api/health` 验证，UI / 路由改动在浏览器确认。若你改动了 `packages/@openmaic/*` 的**源码**，要先重建对应包的 `dist/`（消费方解析的是 `dist/` 不是 `src/`；依赖顺序 `dsl → generation → storage → importer → renderer → editor`，`pnpm install` 的 postinstall 已按此顺序构建，单包可用各自 `pnpm run bui\n\nArchive v0.3.8: 11 files, 22818 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7272b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (7630b), references/live-demo.md (2742b), references/provider-keys.md (5385b), references/startup-modes.md (1192b), skill-card.md (1989b), SKILL.md (6324b), _meta.json (127b)\n\nArchive v0.3.7: 11 files, 23038 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7151b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (7630b), references/live-demo.md (2742b), references/provider-keys.md (5385b), references/startup-modes.md (1192b), skill-card.md (2772b), SKILL.md (6324b), _meta.json (127b)\n\nArchive v0.3.6: 11 files, 22739 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7151b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (6660b), references/live-demo.md (2742b), references/provider-keys.md (5385b), references/startup-modes.md (1192b), skill-card.md (2838b), SKILL.md (6324b), _meta.json (127b)\n\nArchive v0.3.5: 11 files, 22568 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7151b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (6055b), references/live-demo.md (2742b), references/provider-keys.md (5385b), references/startup-modes.md (1192b), skill-card.md (2982b), SKILL.md (6324b), _meta.json (127b)\n\nArchive v0.3.4: 11 files, 22409 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7151b), references/extend-sdk.md (5437b), references/extend.md (7231b), references/generate-flow.md (6055b), references/live-demo.md (2742b), references/provider-keys.md (5300b), references/startup-modes.md (1192b), skill-card.md (2742b), SKILL.md (6324b), _meta.json (127b)\n\nArchive v0.3.3: 11 files, 22568 bytes\n\nFiles: references/clone.md (886b), references/extend-cookbook.md (7151b), references/extend-sdk.md (5437b), references/extend.md (7228b), references/generate-flow.md (6055b), references/live-demo.md (2742b), references/provider-keys.md (5300b), references/startup-modes.md (1192b), skill-card.md (3042b), SKILL.md (6324b), _meta.json (127b)\n\nArchive v0.3.2: 8 files, 11997 bytes\n\nFiles: references/clone.md (886b), references/generate-flow.md (6055b), references/live-demo.md (2742b), references/provider-keys.md (5300b), references/startup-modes.md (1192b), skill-card.md (2423b), SKILL.md (5497b), _meta.json (127b)","readmeExcerpt":"Skill: OpenMAIC Owner: wyuc Summary: OpenMAIC assistant for setting up, generating, and extending OpenMAIC. Use when the user wants to use OpenMAIC, generate a multi-agent interactive classroom, or build on / extend / customize OpenMAIC and its @openmaic/* SDK (secondary development, 二开) — covers Live Demo or local setup, startup modes, provider keys, classroom generation, and secondary development (forking, provider","codeSnippets":[],"executableExamples":[{"language":"jsonc","snippet":"{\n  \"skills\": {\n    \"entries\": {\n      \"openmaic\": {\n        \"enabled\": true,\n        \"config\": {\n          \"accessCode\": \"sk-xxx\",\n          \"repoDir\": \"/path/to/OpenMAIC\",\n          \"url\": \"http://localhost:3000\"\n        }\n      }\n    }\n  }\n}"},{"language":"bash","snippet":"git clone https://github.com/THU-MAIC/OpenMAIC.git\ncd OpenMAIC"},{"language":"bash","snippet":"pnpm install"},{"language":"bash","snippet":"npm i @openmaic/dsl@0.8.0 @openmaic/renderer@0.1.0 \\\n         react@^18 react-dom@^18 motion@^11 tailwindcss@^4\n   # only if you render charts / code-highlighted blocks:\n   npm i echarts@^5 shiki@^1"},{"language":"css","snippet":"@import 'tailwindcss';\n   @import '@openmaic/renderer/fonts.css';"},{"language":"tsx","snippet":"import { SlideCanvas } from '@openmaic/renderer';\n   import type { Slide } from '@openmaic/dsl';"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: openmaic\ndescription: OpenMAIC assistant for setting up, generating, and extending OpenMAIC. Use when the user wants to use OpenMAIC, generate a multi-agent interactive classroom, or build on / extend / customize OpenMAIC and its @openmaic/* SDK (secondary development, 二开) — covers Live Demo or local setup, startup modes, provider keys, classroom generation, and secondary development (forking, providers/storage/themes, routes, or the renderer/editor).\nuser-invocable: true\nmetadata: { \"openclaw\": { \"emoji\": \"🏫\" } }\n---\n\n# OpenMAIC Skill\n\nUse this as a guided, confirmation-heavy SOP. Do not compress the whole setup into one reply and do not perform state-changing actions without explicit user confirmation.\n\n## Core Rules\n\n- Move one phase at a time.\n- Before any state-changing action, ask for confirmation.\n- If local state already exists, show what you found and ask whether to keep it.\n- Do not assume the OpenClaw agent's own model or API key will be reused by OpenMAIC.\n- OpenMAIC classroom generation uses OpenMAIC's server-side model configuration (`openmaic.yml` and the model settings in the web app).\n- This skill must not rely on any request-time model or provider overrides.\n- Only that server-side configuration may control provider selection and defaults.\n- Do not default to asking the user to paste API keys into chat.\n- Prefer guiding the user to edit local config files themselves.\n- Do not offer to write API keys into config files on the user's behalf.\n- Once setup is complete and the user clearly asks to generate a classroom, do not ask for a second confirmation before submitting the generation job.\n- Keep confirmations for local file reads such as reading a PDF from disk before uploading it.\n\n## Optional Skill Config\n\nIf present, read defaults from `~/.openclaw/openclaw.json` under:\n\n```jsonc\n{\n  \"skills\": {\n    \"entries\": {\n      \"openmaic\": {\n        \"enabled\": true,\n        \"config\": {\n          \"accessCode\": \"sk-xxx\",\n          \"repoDir\": \"/path/to/OpenMAIC\",\n          \"url\": \"http://localhost:3000\"\n        }\n      }\n    }\n  }\n}\n```\n\n- If `accessCode` is present, default to Live Demo mode and skip the mode-selection prompt — unless the user's intent is to extend/build on OpenMAIC (see the exception in Phase 0).\n- Use `repoDir` and `url` only as defaults for local mode.\n- Still confirm before acting.\n\n## SOP Phases\n\n### 0. Choose Mode\n\nFirst check skill config for `accessCode`. If present, announce that a stored access code was found and proceed directly to Live Demo mode (load [references/live-demo.md](references/live-demo.md), skip phases 1–4). Do not ask the user to paste the code again. **Exception:** if the user's stated intent is to extend / build on / customize OpenMAIC or consume the `@openmaic/*` SDK (二次开发 / 二开 / SDK), do not auto-shortcut — go to the extend branch below regardless of `accessCode`. A returning Live Demo user who now wants to do 二开 should be routed to extend, not silently sent back to Live Demo.\n\nIf no"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7cjfn5dkygrges9scanxv97d82x3t1\",\n  \"slug\": \"openmaic\",\n  \"version\": \"0.3.11\",\n  \"publishedAt\": 1791124895081\n}"},{"path":"references/clone.md","content":"# Clone Or Reuse Existing Repo\n\n## Goal\n\nEstablish which OpenMAIC checkout will be used for setup and runtime actions.\n\n## Procedure\n\n1. Check whether OpenMAIC already exists locally.\n2. If a checkout exists, show the path and ask whether to reuse it.\n3. If no checkout exists, propose cloning the repo and ask for confirmation.\n4. After clone, confirm dependency installation separately.\n\n## Recommended Path\n\n- Recommended: reuse an existing checkout if it is already on the target branch.\n- Otherwise: clone a fresh checkout from GitHub, then install dependencies.\n\n## Commands\n\nClone:\n\n```bash\ngit clone https://github.com/THU-MAIC/OpenMAIC.git\ncd OpenMAIC\n```\n\nInstall dependencies:\n\n```bash\npnpm install\n```\n\n## Confirmation Requirements\n\n- Ask before `git clone`.\n- Ask before `pnpm install`.\n- If the repo is dirty, tell the user and ask whether to continue with that checkout."},{"path":"references/extend-cookbook.md","content":"# Extend The OpenMAIC Product (Cookbook)\n\n## Scope\n\nYou are working **inside a fork of the OpenMAIC product** (the Next.js app at the repo root), customizing it in place. If instead you want to consume `@openmaic/*` in a separate app, use [extend-sdk.md](extend-sdk.md) instead.\n\nEntry points below are given as **file + symbol name** (not line numbers — they drift). Read the file, then jump to the symbol. The `@/*` path alias is anchored at the repo root.\n\n## Task 1 — Swap Or Add An AI Provider\n\n**Goal:** route generation to a different provider, or register a brand-new one.\n\nTwo distinct cases:\n\n- **Use an already-supported provider** (it's in the union below): no source change. Declare it in `openmaic.yml` with its preset and assign it to a slot, key in `.env.local` as `${VAR}` — follow [provider-keys.md](provider-keys.md). Chat slots always name `<provider id>:<model id>`.\n- **Register a NEW provider** (source change):\n  1. Add the id to the `BuiltInProviderId` union in `lib/types/provider.ts`.\n  2. Register its config + models in the `PROVIDERS` registry in `lib/ai/providers.ts`.\n  3. The registry entry becomes a preset automatically (`lib/config/provider-presets.ts`; the preset id is the registry id unless `lib/config/preset-ids.ts` overrides it), so `openmaic.yml` can declare it with `preset: your-id` and the model settings offer it. No env wiring is needed.\n  4. Optional, legacy only: to also accept `<PREFIX>_API_KEY` / `_BASE_URL` / `_MODELS` from the environment without `openmaic.yml`, add a `PREFIX: 'your-id'` entry to `LLM_ENV_MAP` in `lib/server/provider-config.ts`. That path is deprecated.\n  5. A new token plan (one key, several capabilities) is one entry in `lib/config/token-plan-presets.ts`; it becomes a preset whose recommended models fill the slots it covers in the first-run setup.\n\n**Gotcha:** OpenMAIC has **no hardcoded model fallback**. If no model is assigned to the `llm` slot (or the more specific slot a call uses), generation fails with `No model is configured for <slot>` rather than picking a vendor — always assign one.\n\n## Task 2 — Server-Side Persistence (PostgreSQL / S3)\n\n**Goal:** understand where documents, runtime state and assets live, and point them at your database.\n\nAccurate topology (there is **no local-file backend**, and no browser-storage mode):\n\n- **Always server-backed:** documents, runtime state and assets are stored on the server. The server **refuses to start without `DATABASE_URL`**; locally, `pnpm db:up` starts a separate development PostgreSQL (its own Compose project and volume) on `127.0.0.1` and `.env.example` carries the matching `DATABASE_URL`.\n- **Client side:** `lib/persistence/bootstrap.ts` configures the browser's HTTP-backed `HttpRuntimeStore` / `HttpDocumentStore` / `HttpAssetStore`, which call `/api/persistence`. Those calls carry no credential of their own: the server attributes them to the owner the owner identity seam (`lib/server/identity/`) resolves, and the runtime learner key is that"},{"path":"references/extend-sdk.md","content":"# Consume The @openmaic/* SDK (Build A New App)\n\n## Scope\n\nYou are building a **separate app** that consumes `@openmaic/*` packages via `npm install` — not working inside the OpenMAIC monorepo. If you are customizing the OpenMAIC product itself, use [extend-cookbook.md](extend-cookbook.md) instead.\n\nThe SDK is early-stage (`0.x`). Versions below are current as of this writing — confirm against the registry when you install.\n\n## Package Quick Reference\n\n| Package | Version | Purpose | Key Exports | Peer Deps |\n|---|---|---|---|---|\n| `@openmaic/dsl` | 0.8.0 | The slide **contract** — types + JSON schema. Zero runtime deps. | `Slide`, `PPTElement`, `./schema/*` | none |\n| `@openmaic/renderer` | 0.1.0 | Render slides to DOM (read-only). | `SlideCanvas`, `./snapshot`→`slideToPng` | react ≥18, react-dom ≥18, motion ≥11, tailwindcss ≥4; **optional:** echarts ≥5, shiki ≥1 |\n| `@openmaic/editor` | 0.0.2 | Editable slide surface + prosemirror editor. | `EditableSlideCanvasWithUI` (from `./ui`) | react ≥18, react-dom ≥18 **+ renderer's full peer stack transitively** |\n| `@openmaic/generation` | 0.3.0 | LLM-driven scene/lesson generation. | `generateSceneContent` | none |\n| `@openmaic/storage` | 0.2.5 | Document / Runtime / Asset / KV stores (Browser · HTTP · PG · S3). | see table below | **optional:** `@aws-sdk/client-s3` |\n| `@openmaic/importer` | 0.1.2 | Import PPTX / PDF into the DSL. | `importPptx` | none |\n\n> `editor` is `0.0.2`. Its own `peerDependencies` list only `react`/`react-dom`, but it `dependencies` on `@openmaic/renderer`, so you must also satisfy the renderer's peers (tailwindcss v4, motion, and optionally echarts/shiki) or it breaks at render time.\n\n## Minimal Starter — Render A Slide\n\n1. Install peers and the package (pin exact versions for `0.x`):\n   ```bash\n   npm i @openmaic/dsl@0.8.0 @openmaic/renderer@0.1.0 \\\n         react@^18 react-dom@^18 motion@^11 tailwindcss@^4\n   # only if you render charts / code-highlighted blocks:\n   npm i echarts@^5 shiki@^1\n   ```\n2. Mandatory CSS — without it the canvas renders unstyled:\n   ```css\n   @import 'tailwindcss';\n   @import '@openmaic/renderer/fonts.css';\n   ```\n   ...and ensure Tailwind v4 scans the renderer's classes, e.g. `@source '../node_modules/@openmaic/renderer/dist';` in your globals. (`fonts.css` is generated and fetches CJK woff2 on demand from `https://file.maic.chat/fonts/<name>.woff2` — relevant for offline/custom-font needs.)\n3. Render:\n   ```tsx\n   import { SlideCanvas } from '@openmaic/renderer';\n   import type { Slide } from '@openmaic/dsl';\n   ```\n   For a working example of props/usage, read `components/slide-renderer/SlideThumbnail.tsx` in the OpenMAIC repo.\n\n## Precise Import Paths\n\n| Want | Import |\n|---|---|\n| Slide / element types | `@openmaic/dsl` (`Slide`, `PPTElement`) |\n| JSON schema (validation) | `@openmaic/dsl/schema/*` |\n| Render a slide | `@openmaic/renderer` (`SlideCanvas`) |\n| Snapshot a slide → PNG | `@openmaic/renderer/snapshot` (`slideToPng`) |\n| Editable "}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2085,"uniquenessScore":40,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-09T03:25:13.136Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-09T03:25:13.136Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-09T14:59:27.564Z","emptyReason":null},"items":[{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-10T18:48:31.762Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}