# ChatRoom Plugin System — AI Spec

> Target: AI code generation for `.ctpark` / `.mlpack` plugins.
> Loader: v=20260913+. All field/API names are case-sensitive.

---

## 0. 喂给 AI 的提示词模板

以下文档是专门给 AI 看的，请严格按照提示词喂给 AI，本文档适用于 0 基础小白。先把开发文档发给 AI，然后对 AI 说：

> 你是一个小黑窝聊天室插件开发助手。请先仔细阅读以下插件系统规范（AI Spec），然后严格按照规范为我生成一个 [插件类型] 插件。
>
> [这里填写具体需求]
>
> 一、身份声明
>
> 插件ID: [只能以 字母 和 - 来填写]
> 插件名称：[随意]
> 插件版本：[按情况]
> 描述：[插件功能描述]
> 作者：[填你自己]
> 分类：[随意，可空]
> 主题模式：[深色/浅色/通用]（如果是非主题类插件空着就行）
> 所需权限：[不懂可不填]
>
> 二、功能逻辑：
>
> [按需求分清楚 需用序号标注一、二、三]
>
> 三、生成规则：
>
> [按需求填写 不懂可不写]
>
> 四、输出格式
>
> [*.mlpack/*.ctpart 二选一]
>
> 五、
>
> 按照规则开始生成  [无需动，照搬即可]
>
> 这个括号 [   ] 是给你提示，发给 AI 请删除括号 [   ]

---

## 1. Manifest (`.ctpark` = single JSON; `.mlpack` = ZIP of `manifest.json` + `main.js` + `style.css` + `assets/`)

```ts
interface Manifest {
  id: string;                      // required, lowercase+kebab, globally unique
  name: string;                    // required
  version: string;                 // required, SemVer
  description?: string;
  author?: string;
  category?: "theme";              // "theme" = theme plugin
  themeMode?: "light" | "dark";    // theme plugin only: lock color mode
  type?: "plugin" | "miniprogram"; // default "plugin"
  permissions?: Permission[];      // see §2
  ui?: UIEntry;                    // see §3; ignored for theme plugins
  css?: string; js?: string;       // inline; take priority over *File
  cssFile?: string; jsFile?: string;
  html?: string;                   // modal-mode body
  hooks?: { onLoad?: string; onUnload?: string }; // window fn names
  protocol?: { v: 1; sig: string }; // HMAC-SHA256 hex, see §7
  replaceImages?: Record<string, string>; // theme: CSS selector -> assets/ file or URL
  tabBar?: TabBarConfig; pages?: { content: string; path?: string }[]; // miniprogram only
  entry?: string;                  // .mlpack only: name of entry fn defined in main.js
}
```

## 2. Permissions → APIs (undeclared = `undefined` on `ctx`; self-check via `ctx.permissions: string[]`)

| permission    | ctx API                                                          |
|---------------|------------------------------------------------------------------|
| `storage`     | `storage: { get, set, remove, keys, clear }`                     |
| `network`     | `fetch(url, options?)` → `Promise<Response>`                     |
| `notify`      | `notify(title, options?)` → `Promise<boolean>`                   |
| `clipboard`   | `clipboard: { write(text), read() }`                             |
| `audio`       | `playSound(source: string \| SoundConfig)`                       |
| `theme`       | `getTheme()` + `onThemeChange(cb)`                              |
| `interplugin` | `sendToPlugin(id, msg)` + `onPluginMessage(cb)` + `getPlugin(id)` + `listPlugins()` |
| `timers`      | `setTimeout(fn, delay)` + `setInterval(fn, delay)` + `clearTimer(id)` |
| `dom`         | `getMountContainer(mount): HTMLElement`                          |

## 3. UI Entry (one static entry per manifest; use `ctx.addEntry()` for more)

```ts
interface UIEntry {
  entry: "button";
  mount?: "toolbar" | "sidebar" | "floating" | "panel" | "input-toolbar" | "context-menu" | "modal";
  label?: string; icon?: string;        // Font Awesome class
  action?: string;                      // window fn name; signature (ctx, msgCtx?) => void
  category?: string; order?: number;    // order default 0
  mobileMode?: "miniprogram";           // multi-end: phone auto-opens as miniprogram
}
interface MessageContext { element: HTMLElement; messageId: string; message: { content?: string; text?: string } }
```

## 4. Unprivileged APIs

```ts
ctx.pluginId: string
ctx.permissions: string[]
ctx.showToast(msg: string, type?: "info"|"success"|"error"|"warning"): void
ctx.openModal(dom: HTMLElement): void; ctx.closeModal(): void
ctx.openFloatingCard(cfg: FloatingCardConfig): string   // returns cardId
ctx.closeFloatingCard(cardId: string): boolean
ctx.sendMessage(msg: string): void
ctx.getCurrentUser(): { username: string; avatar?: string }
ctx.on(ev: string, cb: Function): void; ctx.emit(ev: string, data?: any): void
ctx.addEntry(cfg: EntryConfig): string; ctx.removeEntry(id: string): boolean
ctx.updateEntry(id: string, patch: Partial<EntryConfig>): boolean
ctx.getEntries(filter?: { mount?: string; ownerPlugin?: string; source?: string }): EntryConfig[]
ctx.registerFloatingButton(cfg): string; ctx.registerInputButton(cfg): string
ctx.registerContextMenuItem(cfg): string; ctx.registerPanelItem(cfg): string
ctx.openCommandPalette(): void; ctx.openPluginCenter(): void
ctx.setFloatingPosition(id: string, x: number, y: number): boolean
ctx.getFloatingPosition(id: string): { x: number; y: number } | null
ctx.resetFloatingPosition(id: string): void
ctx.isMobile: boolean
ctx.ensureTouchSupport(el: HTMLElement): void
ctx.attachTouchClick(el: HTMLElement, handler: Function, options?): () => void
```

```ts
interface FloatingCardConfig {
  id?: string; title?: string; width?: number; height?: number; x?: number; y?: number;
  content?: string | HTMLElement | ((body: HTMLElement) => void);
  closable?: boolean; resizable?: boolean; onClose?: () => void;
}
interface EntryConfig {
  id?: string; ownerPlugin?: string; mount: string; label: string; icon?: string;
  action: ((ctx: any, msgCtx?: MessageContext) => void) | string;
  category?: string; order?: number; source?: "static" | "dynamic";
}
```

## 5. Privileged API signatures

```ts
// storage (namespaced under localStorage key prefix plugin:{pluginId}:storage:)
ctx.storage.get(k: string): any | null; ctx.storage.set(k: string, v: any): void
ctx.storage.remove(k: string): void; ctx.storage.keys(): string[]; ctx.storage.clear(): void

// network: default timeout 15000ms; options accepts standard fetch options + options.timeout
ctx.fetch(url: string, options?): Promise<Response>

// notify: returns false if permission not granted
ctx.notify(title: string, options?: { body?: string; icon?: string; tag?: string; onclick?: () => void }): Promise<boolean>

// audio
ctx.playSound(src: string | { freq?: number; duration?: number; type?: "sine"|"square"|"sawtooth"|"triangle"; volume?: number }): void

// theme
ctx.getTheme(): "light" | "dark"
ctx.onThemeChange(cb: (t: "light"|"dark") => void): void   // auto-removed on unload

// interplugin
ctx.sendToPlugin(targetId: string, msg: any): boolean
ctx.onPluginMessage(cb: (fromId: string, msg: any) => void): void
ctx.getPlugin(id: string): { id: string; name: string; version: string; description?: string } | null
ctx.listPlugins(): { id: string; name: string; version: string; description?: string }[]
```

## 6. Miniprogram mode

| type           | mobileMode     | desktop            | mobile              |
|----------------|----------------|--------------------|---------------------|
| `"plugin"`     | `"miniprogram"`| run action         | full-screen miniprogram |
| `"miniprogram"`| —              | full-screen (suggest mobile) | full-screen |

```ts
ctx.setNavigationBarTitle(title: string): void
ctx.showNavigationBarBackButton(show: boolean): void
ctx.switchTab(index: number): void     // tabBar page index (0-based)
ctx.navigateTo(url: string): void      // reserved
ctx.navigateBack(delta?: number): void // reserved
ctx.closeMiniprogram(): void
```
`tabBar.list` requires >= 2 items; `pages` order maps 1:1 to `tabBar.list`.

## 7. Trusted-plugin protocol

```
payload = id + "|" + name + "|" + version
sig     = HMAC-SHA256(payload, "lxh-plugin-protocol-2026")   // 64-char hex
```
Add `{ "protocol": { "v": 1, "sig": "<hex>" } }` at manifest root. Trusted plugins: `ctx.isTrusted === true`; skip JS syntax check (supports module/importmap); auto-granted all permissions.

## 8. Theme plugin rules

- Set `category: "theme"`; do not set `ui`/`js`/`action`. Installed disabled by default; only one active at a time.
- Override CSS vars under `:root, [data-theme="light"], [data-theme="dark"]`.
- These 8 vars must use `!important` (override inline styles): `--primary`, `--primary-hover`, `--primary-active`, `--primary-light`, `--primary-dark`, `--primary-rgb`, `--primary-color`, `--color-primary-rgb`.
- Override hard-coded backgrounds with `!important`; global decorations on `body` should set `pointer-events: none`.

## 9. Lifecycle & cleanup

`applyPlugin` → inject CSS/JS → mount UI → register entries/hooks. `disable` removes DOM/entries and clears managed resources (preserves data). `remove` additionally deletes pluginData and localStorage.

Auto-cleaned (no manual work): `ctx.setTimeout/setInterval`, `ctx.onThemeChange`, `ctx.onPluginMessage`, dynamic/static entry DOM, injected CSS/JS tags.
Manual cleanup needed: `ctx.on()` listeners, DOM inserted via `getMountContainer`, native `setTimeout/setInterval`, globals attached to `window`/`document` — handle in `hooks.onUnload`.

## 10. Code-generation rules for AI

1. Mount action fns on `window`: `window.<pluginId>Action = function(ctx, msgCtx) { ... }`.
2. Prefix function names and CSS classes with the plugin `id` to avoid collisions.
3. `try/catch` all async work; report errors via `ctx.showToast(err.message, "error")`.
4. Use `ctx.setInterval` (not native `setInterval`) for any timer that should stop on unload.
5. Declare only permissions actually used.
6. For `context-menu` entries, guard `if (!msgCtx) return;` inside the action.
7. Theme plugin = `css` + `category: "theme"` only; no `ui`/`js`/`action`.
8. `.mlpack`: define the fn named by `manifest.entry` in `main.js` with signature `(ctx) => void`.
9. Do not prefix `storage` keys (loader adds namespace automatically).

