diff --git a/skills/inkreach-teable-plugin/SKILL.md b/skills/inkreach-teable-plugin/SKILL.md new file mode 100644 index 0000000..3e8ee5e --- /dev/null +++ b/skills/inkreach-teable-plugin/SKILL.md @@ -0,0 +1,125 @@ +--- +name: inkreach-teable-plugin +description: >- + Develop dynamic React component plugins for inkreach-teable — IIFE bundles + loaded at runtime via window.__TEABLE_HOST__, no iframe sandbox, full host + API access. Scaffold with plugin-cli, build with webpack, deploy via + upload-bundle API, register via definePlugin + PluginContext. Positions: + contextMenu, panel, view, dashboard, route. Use this whenever the user + mentions plugins, 插件, batch-edit, context-menu actions, panel widgets, + dashboard charts, custom views, plugin.json, definePlugin, bundle plugin, + plugin-cli, plugin-sdk, plugin-host, or window.__TEABLE_HOST__. Even if + the user just says "add a right-click action" or "make a side panel" or + "I need a dashboard widget" — or wants the native form embedded or + extended inside a plugin (原生表单, 表单注入, 表单虚拟字段, custom form + layout, NativeForm, NativeFormScope, form panel) — load this skill. + Reference example at plugins/batch-edit-pro/. +--- + +# inkreach-teable dynamic plugin development + +This skill develops React component plugins that load at runtime without +rebuilding the host Next.js app. Plugins are IIFE bundles — they ship as plain +JS that executes in the host page and accesses host APIs through +`window.__TEABLE_HOST__`. + +> **Contract**: A plugin = a webpack IIFE bundle (`dist/main.js` + optional +> `dist/styles.css`) + `plugin.json` manifest, uploaded to the host via +> `POST /api/plugin/:id/upload-bundle`. It runs in the same React tree as the host +> and has full access to the host's SDK, UI library, and OpenAPI helpers. +> +> **Critical constraint**: NEVER `import` from `@teable/sdk`, `@teable/ui-lib`, +> `@teable/openapi` or `react` in plugin code. These are mapped to +> `window.__TEABLE_HOST__.*` by webpack externals at build time — importing +> them produces `undefined` at runtime. Always read from the host global: +> `const { useFields } = window.__TEABLE_HOST__.SDK`. See +> `dynamic-plugins/troubleshooting.md` Pitfall 1 for details and symptoms. + +## 1. When to use this skill + +- User says "写个插件 / 做个插件 / make a plugin" — yes. +- User wants a context-menu action (batch edit / process selected records) — yes. +- User wants a panel gadget (real-time stats / summary) — yes. +- User wants a custom view (kanban, gallery, timeline) — yes. +- User wants a dashboard chart — yes. +- User wants to embed / extend the NATIVE FORM in a plugin (原生表单, form + injection, virtual form fields, custom multi-step form with native field + editors, `host.NativeForm`, `NativeFormScope`) — yes, see `sdk-api.md`. +- User mentions `definePlugin`, `plugin-cli`, `plugin-sdk`, `window.__TEABLE_HOST__`, bundle plugin — yes. +- User wants to manage tables/automations (not plugins) — use `inkreach-teable` skill instead. + +## 2. Module map (read in order when developing) + +| Stage | File | Read it when... | +|---|---|---| +| 1. Understand architecture | `dynamic-plugins/overview.md` | always first — how `window.__TEABLE_HOST__`, PluginLoader, PluginSlot work | +| 2. Dev workflow | `dynamic-plugins/dev-workflow.md` | when building a plugin — the step-by-step recipe from scaffold to deploy | +| 3. SDK API | `dynamic-plugins/sdk-api.md` | when writing plugin logic — definePlugin, PluginContext, events, storage, useSelection, native form embedding (`NativeForm` Tier-1 injection / `NativeFormScope`+`useNativeForm` Tier-2 kit), NativeGrid | +| 4. Deployment | `dynamic-plugins/deployment.md` | when uploading + enabling + authority matrix permissions | +| 5. Troubleshooting | `dynamic-plugins/troubleshooting.md` | before writing logic — common pitfalls (host globals, developer whitelist, AM, empty states) | +| Ref: batch-edit-pro | `plugins/batch-edit-pro/` | the canonical example — contextMenu plugin, full React + SDK | + +## 3. Quick decision: which position? + +| User wants | Position | `definePlugin` registration | +|---|---|---| +| Right-click action on records | **ContextMenu** | `ctx.addContextMenuAction(id, component)` | +| Side gadget while viewing a table | **Panel** | `ctx.addPanel(id, component)` | +| A new view type | **View** | `ctx.addView(id, component)` | +| Card on the space dashboard | **Dashboard** | `ctx.addDashboardWidget(id, component)` | +| A new page/route | **Route** | `ctx.addRoute(path, component)` | + +## 4. Auth & test credentials + +- Endpoint: `http://localhost:3003` (backend; `:3000` is taken by an unrelated + docker container on this host — see AGENTS.md), frontend `http://localhost:3002`. +- Test account: create a throwaway via `POST /api/auth/signup` — the old + `hr_admin@xingchen.tech` account no longer exists on this instance. Plugin + upload additionally requires a `plugin_developer` row for the uploading user + (`INSERT INTO plugin_developer (id, user_id, email, created_time, created_by) + VALUES ('plgd', '', '', now(), '')`). +- Test base: `bseAi6Hazmw7jPWZbfO`, test table: `tbldtYd4ZhvkcyAaada`. +- Auth: session cookie `auth_session`, or `inkreach-teable auth login`. + +## 5. Start here + +For any plugin request, read `dynamic-plugins/overview.md` then +`dynamic-plugins/dev-workflow.md`. Reference `plugins/batch-edit-pro/` for a +complete working example. + +## 6. Plugin backend services — MUST use the same-origin proxy (2026-08-21) + +The instance is publicly served at `https://table.inkreach.cc` (frp + cloud Caddy); +browsers on the public internet CANNOT reach LAN addresses like +`http://192.168.124.92:8001`. Plugin bundles that need their own backend service +MUST call it through the session-gated dynamic proxy instead: + +- **URL contract**: `fetch('/api/plugin-proxy//')` — same-origin, + works from LAN and public internet alike. NEVER hardcode LAN IPs / `localhost` + in a bundle. +- **Registry**: one line in `PLUGIN_API_SERVICES` (JSON, in + `apps/nextjs-app/.env` AND `.env.development`), then restart + `teable-backend.service`. No rebuild, no code change. + Example: `PLUGIN_API_SERVICES={"track":"http://127.0.0.1:8001"}` +- **Implementation**: NestJS controller + `apps/nestjs-backend/src/features/plugin/plugin-proxy.controller.ts` + (global AuthGuard gates login; upstream whitelist = registry; session cookies + are never forwarded upstream). Do NOT move this to a Next.js pages-router API + route — the `/api/:path*` rewrite in `next.config.js` outranks catch-all file + routes, and files outside `pages/api/` get compiled as SSR pages (observed on + Next 16.1.6/turbopack). +- **CSP**: same-origin fetches need nothing; truly external origins must be + declared in the plugin manifest `apiOrigins` (appended to connect-src by + `plugin-runtime.controller.ts`). +- Re-uploading a plugin zip OVERWRITES the bundle — re-check for hardcoded LAN + URLs afterwards (`grep -o 'http://192.168[^\"']*' /main.js`). + Live example: `apps/nestjs-backend/.assets/plugins/bundle/track-panel/` + (patched from LAN URL to `/api/plugin-proxy/track`; backups kept as + `main.js.bak-*`). +- **Root fix (2026-08-22 recurrence)**: a developer re-uploaded track-panel at + 02:23 with the LAN URL back in the source-built bundle, breaking public + search with a CORS error within hours of the last patch. Patching + `main.js` on disk only fixes the CURRENT artifact — the plugin SOURCE must + use `/api/plugin-proxy/track` before building/uploading, otherwise every + re-upload reintroduces the bug. The backend serves bundle files from disk + per-request, so a disk patch takes effect without a backend restart. diff --git a/skills/inkreach-teable-plugin/dynamic-plugins/deployment.md b/skills/inkreach-teable-plugin/dynamic-plugins/deployment.md new file mode 100644 index 0000000..bf76d16 --- /dev/null +++ b/skills/inkreach-teable-plugin/dynamic-plugins/deployment.md @@ -0,0 +1,194 @@ +# Plugin deployment + +Uploading a bundle plugin, enabling it, and managing permissions. + +## Upload — `POST /api/plugin/:pluginId/upload-bundle` + +Upload the built bundle to the host. Uses `multipart/form-data`. +The `pluginId` in the URL must match the `id` in `plugin.json`. + +```bash +PLUGIN_ID=$(python3 -c "import json; print(json.load(open('plugin.json'))['id'])") +curl -s -b "auth_session=$SESSION" \ + -F "js=@dist/main.js" \ + -F "css=@dist/styles.css" \ + -F "manifest=@plugin.json" \ + "http://localhost:3003/api/plugin/$PLUGIN_ID/upload-bundle" +``` + +**Auth note**: Supports both session cookies (`auth_session`) and Personal Access +Tokens (`Authorization: Bearer `). + +**Fields:** +| Field | Required | Max size | Description | +|---|---|---|---| +| `js` | Yes | 5 MB | The IIFE bundle (`dist/main.js`) | +| `css` | No | 1 MB | Optional stylesheet (`dist/styles.css`) | +| `manifest` | Yes | 10 KB | `plugin.json` manifest | + +**Returns** `201` with `{ id, name, version, ... }` — the plugin ID to use for +enable and install. + +**Security** (7-layer validation): +1. Authentication — valid session cookie required (401 if missing) +2. Developer whitelist — `plugin_developer` table lookup (403 if not whitelisted) +3. Rate limit — 10 uploads/min per user (429 if exceeded) +4. Size limits — JS ≤ 5 MB, CSS ≤ 1 MB, manifest ≤ 10 KB +5. Manifest validation — required fields, `id` regex `[a-z0-9-]+` +6. Content validation — JS must be IIFE format (prefix `!function`/`(()=>`), + rejects `