@plumeai/plugin-sdk 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/LICENSE +21 -0
  2. package/bin/plume-plugin.mjs +70 -0
  3. package/dist/client/styles/src/index.d.ts +123 -0
  4. package/dist/client/ui/src/avatar/AgentAvatar.d.ts +42 -0
  5. package/dist/client/ui/src/avatar/AvatarPile.d.ts +36 -0
  6. package/dist/client/ui/src/avatar/AvatarStack.d.ts +26 -0
  7. package/dist/client/ui/src/avatar/art.d.ts +32 -0
  8. package/dist/client/ui/src/fileIcon.d.ts +33 -0
  9. package/dist/client/ui/src/hooks/useCopied.d.ts +16 -0
  10. package/dist/client/ui/src/hooks/useLiveRefresh.d.ts +22 -0
  11. package/dist/client/ui/src/hooks/useNarrow.d.ts +16 -0
  12. package/dist/client/ui/src/i18n/context.d.ts +17 -0
  13. package/dist/client/ui/src/i18n/index.d.ts +2 -0
  14. package/dist/client/ui/src/i18n/list.d.ts +20 -0
  15. package/dist/client/ui/src/i18n/locales.d.ts +10 -0
  16. package/dist/client/ui/src/i18n/messages/en.d.ts +1418 -0
  17. package/dist/client/ui/src/i18n/messages/index.d.ts +6 -0
  18. package/dist/client/ui/src/i18n/messages/zh-CN.d.ts +1263 -0
  19. package/dist/client/ui/src/i18n/plural.d.ts +16 -0
  20. package/dist/client/ui/src/i18n/translate.d.ts +43 -0
  21. package/dist/client/ui/src/icons/index.d.ts +5 -0
  22. package/dist/client/ui/src/image/AnnotateCanvas.d.ts +29 -0
  23. package/dist/client/ui/src/image/AnnotateToolbar.d.ts +73 -0
  24. package/dist/client/ui/src/image/Annotator.d.ts +154 -0
  25. package/dist/client/ui/src/image/annotate.d.ts +227 -0
  26. package/dist/client/ui/src/image/paint.d.ts +35 -0
  27. package/dist/client/ui/src/image/screenshot-geometry.d.ts +112 -0
  28. package/dist/client/ui/src/image/useBarDrag.d.ts +31 -0
  29. package/dist/client/ui/src/image/useMovableBar.d.ts +26 -0
  30. package/dist/client/ui/src/image/useToolbarPlacement.d.ts +42 -0
  31. package/dist/client/ui/src/index.d.ts +140 -0
  32. package/dist/client/ui/src/inputs/ColorField.d.ts +9 -0
  33. package/dist/client/ui/src/inputs/ComposerToggle.d.ts +23 -0
  34. package/dist/client/ui/src/inputs/DateRangeField.d.ts +25 -0
  35. package/dist/client/ui/src/inputs/NativeField.d.ts +5 -0
  36. package/dist/client/ui/src/inputs/SearchField.d.ts +21 -0
  37. package/dist/client/ui/src/inputs/Segmented.d.ts +23 -0
  38. package/dist/client/ui/src/inputs/Select.d.ts +32 -0
  39. package/dist/client/ui/src/inputs/ShortcutRecorder.d.ts +8 -0
  40. package/dist/client/ui/src/inputs/Slider.d.ts +13 -0
  41. package/dist/client/ui/src/inputs/TextArea.d.ts +30 -0
  42. package/dist/client/ui/src/inputs/Toggle.d.ts +10 -0
  43. package/dist/client/ui/src/inputs/WorktreeToggle.d.ts +19 -0
  44. package/dist/client/ui/src/inputs/day-range.d.ts +57 -0
  45. package/dist/client/ui/src/inputs/pickers.d.ts +40 -0
  46. package/dist/client/ui/src/inputs/useFieldFade.d.ts +1 -0
  47. package/dist/client/ui/src/keyboard.d.ts +21 -0
  48. package/dist/client/ui/src/layout/ApprovalFrame.d.ts +33 -0
  49. package/dist/client/ui/src/layout/Collapse.d.ts +9 -0
  50. package/dist/client/ui/src/layout/CopyMark.d.ts +19 -0
  51. package/dist/client/ui/src/layout/Disclosure.d.ts +66 -0
  52. package/dist/client/ui/src/layout/ErrorBoundary.d.ts +26 -0
  53. package/dist/client/ui/src/layout/PageTopRow.d.ts +19 -0
  54. package/dist/client/ui/src/layout/PanelEmpty.d.ts +17 -0
  55. package/dist/client/ui/src/layout/ResizeHandle.d.ts +24 -0
  56. package/dist/client/ui/src/layout/RetainedViews.d.ts +32 -0
  57. package/dist/client/ui/src/layout/Settings.d.ts +41 -0
  58. package/dist/client/ui/src/layout/Splitter.d.ts +25 -0
  59. package/dist/client/ui/src/lib/agent-avatar.d.ts +58 -0
  60. package/dist/client/ui/src/lib/code/font-stack.d.ts +12 -0
  61. package/dist/client/ui/src/lib/code/themes.d.ts +35 -0
  62. package/dist/client/ui/src/lib/date-format.d.ts +8 -0
  63. package/dist/client/ui/src/lib/dispatches.d.ts +8 -0
  64. package/dist/client/ui/src/lib/hour-style.d.ts +13 -0
  65. package/dist/client/ui/src/lib/number-draft.d.ts +24 -0
  66. package/dist/client/ui/src/lib/relative-time.d.ts +18 -0
  67. package/dist/client/ui/src/lib/tiling.d.ts +89 -0
  68. package/dist/client/ui/src/lib/typing.d.ts +19 -0
  69. package/dist/client/ui/src/motion/RollWords.d.ts +26 -0
  70. package/dist/client/ui/src/motion/RollingText.d.ts +43 -0
  71. package/dist/client/ui/src/motion/TypedText.d.ts +9 -0
  72. package/dist/client/ui/src/motion/freeze.d.ts +22 -0
  73. package/dist/client/ui/src/motion/held.d.ts +17 -0
  74. package/dist/client/ui/src/motion/loaders.d.ts +65 -0
  75. package/dist/client/ui/src/motion/reduced.d.ts +13 -0
  76. package/dist/client/ui/src/motion/ticked.d.ts +24 -0
  77. package/dist/client/ui/src/motion/ticker.d.ts +31 -0
  78. package/dist/client/ui/src/motion/tokens.d.ts +29 -0
  79. package/dist/client/ui/src/motion/useNow.d.ts +6 -0
  80. package/dist/client/ui/src/overlay/Confirm.d.ts +96 -0
  81. package/dist/client/ui/src/overlay/ContextMenu.d.ts +40 -0
  82. package/dist/client/ui/src/overlay/Dialog.d.ts +60 -0
  83. package/dist/client/ui/src/overlay/Menu.d.ts +90 -0
  84. package/dist/client/ui/src/overlay/Overlay.d.ts +17 -0
  85. package/dist/client/ui/src/overlay/Popover.d.ts +127 -0
  86. package/dist/client/ui/src/overlay/Tooltip.d.ts +14 -0
  87. package/dist/client/ui/src/overlay/afterimage.d.ts +20 -0
  88. package/dist/client/ui/src/overlay/chrome-top.d.ts +8 -0
  89. package/dist/client/ui/src/overlay/hover-layers.d.ts +41 -0
  90. package/dist/client/ui/src/overlay/portal.d.ts +21 -0
  91. package/dist/client/ui/src/overlay/scrims.d.ts +20 -0
  92. package/dist/client/ui/src/overlay/tooltip-attribute.d.ts +62 -0
  93. package/dist/client/ui/src/primitives/Button.d.ts +64 -0
  94. package/dist/client/ui/src/primitives/Caret.d.ts +25 -0
  95. package/dist/client/ui/src/primitives/ChoiceMark.d.ts +12 -0
  96. package/dist/client/ui/src/primitives/ClosableTab.d.ts +23 -0
  97. package/dist/client/ui/src/primitives/CountUp.d.ts +21 -0
  98. package/dist/client/ui/src/primitives/GitHubMark.d.ts +12 -0
  99. package/dist/client/ui/src/primitives/IconButton.d.ts +55 -0
  100. package/dist/client/ui/src/primitives/MarkNumber.d.ts +12 -0
  101. package/dist/client/ui/src/primitives/RowDeleteButton.d.ts +6 -0
  102. package/dist/client/ui/src/primitives/Skeleton.d.ts +36 -0
  103. package/dist/client/ui/src/primitives/TabStrip.d.ts +29 -0
  104. package/dist/client/ui/src/primitives/Text.d.ts +341 -0
  105. package/dist/client/ui/src/primitives/TimeAgo.d.ts +13 -0
  106. package/dist/client/ui/src/primitives/useCountUp.d.ts +23 -0
  107. package/dist/client/ui/src/row/FlowRow.d.ts +31 -0
  108. package/dist/client/ui/src/row/HoverRow.d.ts +28 -0
  109. package/dist/client/ui/src/scroll/BackToLatest.d.ts +19 -0
  110. package/dist/client/ui/src/scroll/OverlayScrollbar.d.ts +9 -0
  111. package/dist/client/ui/src/scroll/ScrollText.d.ts +24 -0
  112. package/dist/client/ui/src/scroll/Scroller.d.ts +66 -0
  113. package/dist/client/ui/src/scroll/Sideways.d.ts +30 -0
  114. package/dist/client/ui/src/scroll/fade.d.ts +2 -0
  115. package/dist/client/ui/src/scroll/follow.d.ts +157 -0
  116. package/dist/client/ui/src/scroll/measure-batch.d.ts +13 -0
  117. package/dist/client/ui/src/scroll/memory.d.ts +22 -0
  118. package/dist/client/ui/src/scroll/signature.d.ts +24 -0
  119. package/dist/client/ui/src/scroll/useFollowBottom.d.ts +80 -0
  120. package/dist/client/ui/src/scroll/useSideways.d.ts +29 -0
  121. package/dist/contract/src/app.d.ts +16 -0
  122. package/dist/contract/src/changes.d.ts +50 -0
  123. package/dist/contract/src/diagnostics.d.ts +86 -0
  124. package/dist/contract/src/index.d.ts +18 -0
  125. package/dist/contract/src/plugin.d.ts +1391 -0
  126. package/dist/contract/src/resources.d.ts +79 -0
  127. package/dist/contract/src/service.d.ts +806 -0
  128. package/dist/contract/src/ui.d.ts +1039 -0
  129. package/dist/contract/src/usage.d.ts +102 -0
  130. package/dist/core/llm/ai/anthropic-messages-request.d.ts +82 -0
  131. package/dist/core/llm/ai/anthropic-messages.d.ts +43 -0
  132. package/dist/core/llm/ai/cache-routing.d.ts +53 -0
  133. package/dist/core/llm/ai/chat-delta.d.ts +107 -0
  134. package/dist/core/llm/ai/compat-key.d.ts +19 -0
  135. package/dist/core/llm/ai/endpoint.d.ts +57 -0
  136. package/dist/core/llm/ai/failure.d.ts +181 -0
  137. package/dist/core/llm/ai/index.d.ts +16 -0
  138. package/dist/core/llm/ai/openai-chat-completions-request.d.ts +49 -0
  139. package/dist/core/llm/ai/openai-chat-completions.d.ts +37 -0
  140. package/dist/core/llm/ai/openai-responses-request.d.ts +63 -0
  141. package/dist/core/llm/ai/openai-responses.d.ts +11 -0
  142. package/dist/core/llm/ai/prefix-fingerprint.d.ts +20 -0
  143. package/dist/core/llm/ai/reasoning-compat.d.ts +69 -0
  144. package/dist/core/llm/ai/request-params-compat.d.ts +39 -0
  145. package/dist/core/llm/ai/retry.d.ts +130 -0
  146. package/dist/core/llm/ai/sanitize-history.d.ts +23 -0
  147. package/dist/core/llm/ai/think-tag.d.ts +37 -0
  148. package/dist/core/llm/ai/thinking-options.d.ts +35 -0
  149. package/dist/core/llm/ai/tool-names.d.ts +17 -0
  150. package/dist/core/llm/ai/tool-pairing-compat.d.ts +53 -0
  151. package/dist/core/llm/ai/tool-result-images-compat.d.ts +24 -0
  152. package/dist/core/llm/ai/tool-schema.d.ts +20 -0
  153. package/dist/core/llm/ai/usage-fields.d.ts +30 -0
  154. package/dist/core/llm/ai/vendors/index.d.ts +20 -0
  155. package/dist/core/llm/ai/vendors/thinking.d.ts +39 -0
  156. package/dist/core/llm/ai/vendors/types.d.ts +47 -0
  157. package/dist/core/llm/config/image-models.d.ts +34 -0
  158. package/dist/core/llm/config/retry-policy.d.ts +35 -0
  159. package/dist/core/llm/images/chat-images.d.ts +11 -0
  160. package/dist/core/llm/images/dimensions.d.ts +15 -0
  161. package/dist/core/llm/images/generate.d.ts +15 -0
  162. package/dist/core/llm/images/openai-images.d.ts +11 -0
  163. package/dist/core/llm/images/quota.d.ts +9 -0
  164. package/dist/core/llm/images/shape.d.ts +44 -0
  165. package/dist/core/llm/images/types.d.ts +118 -0
  166. package/dist/core/llm/model-catalog-format.d.ts +50 -0
  167. package/dist/core/llm/model-catalog.d.ts +51 -0
  168. package/dist/core/llm/types/message.d.ts +450 -0
  169. package/dist/core/llm/types/provider.d.ts +169 -0
  170. package/dist/core/llm/types/tool.d.ts +23 -0
  171. package/dist/core/llm/types.d.ts +10 -0
  172. package/dist/core/llm/utils/pricing.d.ts +18 -0
  173. package/dist/core/llm/utils/record.d.ts +2 -0
  174. package/dist/core/llm/utils/sse.d.ts +69 -0
  175. package/dist/infra/rpc/src/endpoint.d.ts +36 -0
  176. package/dist/infra/rpc/src/index.d.ts +2 -0
  177. package/dist/infra/rpc/src/rpc.d.ts +54 -0
  178. package/dist/theme.css +336 -0
  179. package/dist/ui.d.ts +2 -0
  180. package/lib/build.mjs +143 -0
  181. package/lib/check.mjs +23 -0
  182. package/lib/dev.mjs +52 -0
  183. package/lib/icons.mjs +2 -0
  184. package/lib/init.mjs +109 -0
  185. package/lib/install.mjs +80 -0
  186. package/lib/link.mjs +44 -0
  187. package/lib/lint/i18n.mjs +149 -0
  188. package/lib/lint/imports.mjs +83 -0
  189. package/lib/lint/index.mjs +54 -0
  190. package/lib/lint/styles.mjs +160 -0
  191. package/lib/manifest.d.mts +9 -0
  192. package/lib/manifest.mjs +99 -0
  193. package/lib/targets.mjs +60 -0
  194. package/package.json +34 -4
  195. package/skill/SKILL.md +150 -0
  196. package/skill/references/components.md +2830 -0
  197. package/skill/references/logic.md +526 -0
  198. package/skill/references/style-i18n.md +70 -0
  199. package/skill/references/ui.md +532 -0
  200. package/template/AGENTS.md +17 -0
  201. package/template/CLAUDE.md +1 -0
  202. package/template/README.md +12 -0
  203. package/template/gitignore +2 -0
  204. package/template/src/index.ts +28 -0
  205. package/template/src/messages.ts +16 -0
  206. package/template/src/types.ts +8 -0
  207. package/template/src/ui.tsx +30 -0
  208. package/template/tsconfig.json +7 -0
  209. package/tsconfig.json +25 -0
  210. package/README.md +0 -3
@@ -0,0 +1,160 @@
1
+ /**
2
+ * 插件的样式规则(插件检查 `plume-plugin check` 和 Plume 自己的 `pnpm check:styles` 用的是这同一份,约定见 Plume 仓库的 spec/tech/styling.md):
3
+ *
4
+ * - 色值:代码里不写色值(#rrggbb、rgb()……),用 @plume/styles 的组合或 var(--plume-*)。确实和主题无关的文件
5
+ * (画在 canvas 上只认字面色值、画在别人的页面上……)在文件里写一行带 `color-exempt` 的注释,写明原因,整个文件不查色值。
6
+ * - 时长、行内样式:CSS 里的过渡时长只用 var(--plume-t-*);行内 style 里的颜色、圆角、字号、字体、阴影、过渡只取 var(--plume-*)。
7
+ * - 插件类名:插件单独构建,产品扫不到插件自己写的类名,className 里不能有 Tailwind 类,用 @plume/styles 导出的组合。
8
+ * - 插件 CSS 只管布局:字号、行高、字距、字体、字重、圆角、阴影只能是单个 var(--plume-*) 或 inherit 这类关键字。
9
+ *
10
+ * 只看字符串字面量(CSS 文件看全文),注释不查。
11
+ */
12
+
13
+ import { readFile } from "node:fs/promises";
14
+ import { createRequire } from "node:module";
15
+ import { dirname, extname, join, resolve } from "node:path";
16
+
17
+ const COLOR_LITERAL = /#[0-9a-fA-F]{3,8}\b|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(/g;
18
+ /** CSS 里的过渡时长。0.01ms 是减少动态效果那条规则本身。 */
19
+ const CSS_TRANSITION = /transition(?:-duration)?\s*:([^;]*)/g;
20
+ const TIME_LITERAL = /(?<![\w.-])(?!0\.01ms)\d*\.?\d+m?s\b/;
21
+ /** 行内 style 里只能取 var(--plume-*) 的属性;只看字面量值,类型注解不算。 */
22
+ const INLINE_STYLE =
23
+ /\b(color|background|backgroundColor|borderColor|borderRadius|fontSize|lineHeight|letterSpacing|fontFamily|boxShadow|transition|transitionDuration)\s*:\s*(-?\d[\w.%]*|"[^"\n]*"|'[^'\n]*'|`[^`]*`)/g;
24
+ const INLINE_OK = /var\(--plume-|^["'`](?:transparent|inherit|currentColor|none)["'`]$/;
25
+ /** 插件 CSS 里归 token 和组件管的属性:值只能是单个 var(--plume-*) 或关键字,calc 拼档位也不行。 */
26
+ const PLUGIN_CSS_TYPE = /(?<![\w-])(font|font-size|line-height|letter-spacing|font-family|font-weight|border-radius|box-shadow)\s*:\s*([^;}]+)/g;
27
+ const PLUGIN_CSS_OK = /^(?:var\(--plume-[\w-]+\)|inherit|initial|unset|normal|none|0)$/;
28
+ /** 整个文件不查色值的标记,写在注释里,后面跟原因。 */
29
+ const COLOR_EXEMPT = /\bcolor-exempt\b/;
30
+
31
+ /** 字符串字面量的内容和位置;模板字符串里的 ${…} 换成空格(里面的字面量会被单独取到)。 */
32
+ export function stringLiterals(text) {
33
+ const out = [];
34
+ const pattern = /"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|`((?:[^`\\]|\\.)*)`/g;
35
+ for (const match of text.matchAll(pattern)) {
36
+ const body = match[1] ?? match[2] ?? match[3] ?? "";
37
+ out.push({ index: match.index, body: match[3] === undefined ? body : body.replace(/\$\{[^}]*\}/g, " ") });
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** 去掉注释,保留换行,行号不变。字符串里的 // 不动。 */
43
+ export function stripComments(text, css) {
44
+ const pattern = css ? /\/\*[\s\S]*?\*\//g : /\/\*[\s\S]*?\*\/|(?<![:"'`\\])\/\/[^\n]*|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
45
+ return text.replace(pattern, (match) => (match.startsWith("/") ? match.replace(/[^\n]/g, " ") : match));
46
+ }
47
+
48
+ /**
49
+ * 写死的色值。返回 `{ index, value }`,index 是在原文里的位置。
50
+ * @param {string} text
51
+ * @param {boolean} css
52
+ */
53
+ export function colorLiterals(text, css) {
54
+ const code = stripComments(text, css);
55
+ const chunks = css ? [{ index: 0, body: code }] : stringLiterals(code);
56
+ const found = [];
57
+ for (const { index, body } of chunks) for (const match of body.matchAll(COLOR_LITERAL)) found.push({ index: index + match.index, value: match[0] });
58
+ return found;
59
+ }
60
+
61
+ /**
62
+ * 行内 style 和 CSS 里的过渡时长。返回 `{ index, rule, message }`。
63
+ * @param {string} text
64
+ * @param {boolean} css
65
+ */
66
+ export function inlineProblems(text, css) {
67
+ const code = stripComments(text, css);
68
+ const found = [];
69
+ if (css) {
70
+ for (const match of code.matchAll(CSS_TRANSITION)) {
71
+ const time = match[1].match(TIME_LITERAL);
72
+ if (time) found.push({ index: match.index, rule: "时长", message: `${time[0]}:过渡时长用 var(--plume-t-quick / base / slow)` });
73
+ }
74
+ return found;
75
+ }
76
+ for (const match of code.matchAll(INLINE_STYLE)) {
77
+ if (!INLINE_OK.test(match[2]))
78
+ found.push({
79
+ index: match.index,
80
+ rule: "行内样式",
81
+ message: `${match[1]}: ${match[2]}:行内样式的颜色、圆角、字号、字体、阴影、过渡只取 var(--plume-*)`,
82
+ });
83
+ }
84
+ return found;
85
+ }
86
+
87
+ /**
88
+ * 插件一个源文件的样式问题。返回 `{ index, rule, message }`。
89
+ * @param {string} file 路径,按扩展名分 CSS 和代码
90
+ * @param {string} text
91
+ * @param {(candidate: string) => boolean} isTailwind 这个类名 Tailwind 生成得了样式
92
+ */
93
+ export function pluginStyleProblems(file, text, isTailwind) {
94
+ const css = extname(file) === ".css";
95
+ const found = [];
96
+ if (!COLOR_EXEMPT.test(text)) {
97
+ for (const { index, value } of colorLiterals(text, css))
98
+ found.push({ index, rule: "色值", message: `${value}:颜色跟着主题和皮肤走,用 @plume/styles 的组合或 var(--plume-*)` });
99
+ }
100
+ found.push(...inlineProblems(text, css));
101
+ for (const match of stripComments(text, css).matchAll(/className=(?:"([^"]*)"|\{\s*"([^"]*)"\s*\}|\{\s*`([^`]*)`\s*\})/g)) {
102
+ const body = (match[1] ?? match[2] ?? match[3] ?? "").replace(/\$\{[^}]*\}/g, " ");
103
+ for (const candidate of body.split(/\s+/).filter(Boolean)) {
104
+ if (isTailwind(candidate))
105
+ found.push({
106
+ index: match.index,
107
+ rule: "插件类名",
108
+ message: `${candidate}:插件单独构建,产品扫不到它自己写的 Tailwind 类,用 @plume/styles 导出的组合`,
109
+ });
110
+ }
111
+ }
112
+ if (css) {
113
+ for (const match of stripComments(text, true).matchAll(PLUGIN_CSS_TYPE)) {
114
+ if (!PLUGIN_CSS_OK.test(match[2].trim()))
115
+ found.push({
116
+ index: match.index,
117
+ rule: "插件样式",
118
+ message: `${match[1]}: ${match[2].trim()}:插件 CSS 只管布局,文字用 @plume/ui 的 Text,圆角、阴影取单个 var(--plume-*)`,
119
+ });
120
+ }
121
+ }
122
+ return found;
123
+ }
124
+
125
+ /**
126
+ * 问一个类名能不能生成样式。compiler 是 Tailwind 的 compile() 的结果;build 是增量的:输出变长了说明这个类名有效。
127
+ * @param {{ build(candidates: string[]): string }} compiler
128
+ * @returns {(candidate: string) => boolean}
129
+ */
130
+ export function tailwindProbe(compiler) {
131
+ let last = compiler.build([]);
132
+ const known = new Map();
133
+ return (candidate) => {
134
+ if (!known.has(candidate)) {
135
+ const next = compiler.build([candidate]);
136
+ known.set(candidate, next !== last);
137
+ last = next;
138
+ }
139
+ return known.get(candidate);
140
+ };
141
+ }
142
+
143
+ /**
144
+ * 和产品一样的 Tailwind:Tailwind 本身加 Plume 的 theme.css(SDK 的 dist/theme.css,生成类型声明时复制进来)。
145
+ * 产品清空了 Tailwind 自带的色板、字号,带上 theme.css 才和产品认的类名一致。
146
+ */
147
+ export async function pluginTailwind() {
148
+ const require = createRequire(import.meta.url);
149
+ const { compile } = await import("tailwindcss");
150
+ const tailwind = dirname(require.resolve("tailwindcss/package.json"));
151
+ const theme = resolve(import.meta.dirname, "../../dist/theme.css");
152
+ const compiler = await compile('@import "tailwindcss" source(none);\n@import "plume-theme";', {
153
+ base: tailwind,
154
+ async loadStylesheet(id, base) {
155
+ const path = id === "tailwindcss" ? join(tailwind, "index.css") : id === "plume-theme" ? theme : resolve(base, id);
156
+ return { path, base: dirname(path), content: await readFile(path, "utf8") };
157
+ },
158
+ });
159
+ return tailwindProbe(compiler);
160
+ }
@@ -0,0 +1,9 @@
1
+ // manifest.mjs 的类型。说明的形状定义在 @plume/contract,这里引 typecheck 生成到 dist/ 的那份声明:发到 npm 的包里没有 @plume/contract。
2
+ import type { PluginManifest } from "../dist/contract/src/index.d.ts";
3
+
4
+ export declare const DIR_NAME: RegExp;
5
+ export declare const PLUGIN_ENTRY: string;
6
+ export declare const SHIPPED: readonly string[];
7
+ export declare function readManifest(raw: unknown): PluginManifest;
8
+ export declare function readCatalogInfo(raw: unknown): (PluginManifest & { name: string }) | null;
9
+ export declare function manifestProblems(raw: unknown): string[];
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 插件包的格式:什么目录算一个插件、发给插件的有哪些文件、`package.json` 里给市场看的说明怎么读、发到市场前必须写齐哪些、
3
+ * 市场目录 `marketplace.json` 的一条怎么读回来。
4
+ *
5
+ * 用到它的几处说的是同一份格式:应用(主进程找插件、读说明、装市场的包,打包时放内置插件)、这里的 `plume-plugin install`
6
+ * 和 `init`、插件仓库发市场(校验、打包写目录)。插件仓库经 npm 上的 SDK 引它(`@plumeai/plugin-sdk/manifest`),
7
+ * 两个仓库不用各写一份。类型(`PluginKind`、`PluginManifest`)在 `@plume/contract`,旁边的 manifest.d.mts 引它。
8
+ * 只用 Node 自带的,不引别的包。
9
+ */
10
+
11
+ /** 插件目录名:也是安装目录名、市场条目的 `name`、`plume-plugin://` 的主机名。 */
12
+ export const DIR_NAME = /^[a-z0-9][a-z0-9-]*$/;
13
+
14
+ /** 逻辑入口:插件目录里有它才算一个插件(没构建的源码目录不算)。 */
15
+ export const PLUGIN_ENTRY = "dist/index.mjs";
16
+
17
+ /**
18
+ * 发给插件的文件:说明、介绍和它引用的图片、图标、构建产物、随包的技能、MCP 服务的声明、许可证。源码、node_modules 不发。
19
+ * 市场的包、`plume-plugin install`、应用里「从本地上传」都只装这些,应用算文件指纹也只看这些。
20
+ */
21
+ export const SHIPPED = ["package.json", "README.md", "images", "icon.png", "dist", "skills", ".mcp.json", "LICENSE"];
22
+
23
+ const KINDS = new Set(["plugin", "skill", "connector"]);
24
+
25
+ /** 读 `package.json` 的内容。字段类型不对或是空的当没写,一个坏字段不拖垮整份;`plume.kind` 没写或写错按普通插件。 */
26
+ export function readManifest(raw) {
27
+ const manifest = record(raw);
28
+ const plume = record(manifest.plume);
29
+ return compact({
30
+ kind: kindOf(plume.kind),
31
+ displayName: text(plume.displayName),
32
+ description: text(manifest.description),
33
+ version: text(manifest.version),
34
+ author: text(manifest.author) ?? text(record(manifest.author).name),
35
+ category: text(plume.category),
36
+ keywords: texts(manifest.keywords),
37
+ homepage: text(manifest.homepage),
38
+ license: text(manifest.license),
39
+ prompts: texts(plume.defaultPrompt),
40
+ });
41
+ }
42
+
43
+ /** 读市场目录里一条的说明部分(打包时写的是 `{ name, ...readManifest(...) }`)。目录名不合规矩的是 null。 */
44
+ export function readCatalogInfo(raw) {
45
+ const entry = record(raw);
46
+ const name = text(entry.name);
47
+ if (!name || !DIR_NAME.test(name)) return null;
48
+ return {
49
+ name,
50
+ ...compact({
51
+ kind: kindOf(entry.kind),
52
+ displayName: text(entry.displayName),
53
+ description: text(entry.description),
54
+ version: text(entry.version),
55
+ author: text(entry.author),
56
+ category: text(entry.category),
57
+ keywords: texts(entry.keywords),
58
+ homepage: text(entry.homepage),
59
+ license: text(entry.license),
60
+ prompts: texts(entry.prompts),
61
+ }),
62
+ };
63
+ }
64
+
65
+ /** 发到市场的插件必须写的说明,缺了哪样:版本号(x.y.z)、显示名、说明、合法的 kind。一样一句。 */
66
+ export function manifestProblems(raw) {
67
+ const manifest = record(raw);
68
+ const plume = record(manifest.plume);
69
+ const problems = [];
70
+ const version = typeof manifest.version === "string" ? manifest.version : "";
71
+ if (!/^\d+\.\d+\.\d+$/.test(version)) problems.push(`version 要写成 x.y.z(现在是 ${version || "空的"})`);
72
+ if (!text(plume.displayName)) problems.push("package.json 没写 plume.displayName(市场里的名字)");
73
+ if (!text(manifest.description)) problems.push("package.json 没写 description");
74
+ if (plume.kind !== undefined && !KINDS.has(plume.kind)) problems.push(`plume.kind 只能是 ${[...KINDS].join("、")}`);
75
+ return problems;
76
+ }
77
+
78
+ function kindOf(value) {
79
+ return KINDS.has(value) ? value : "plugin";
80
+ }
81
+
82
+ function record(value) {
83
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
84
+ }
85
+
86
+ function text(value) {
87
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
88
+ }
89
+
90
+ /** 字符串数组里非空的那些;一个都没有算没写。 */
91
+ function texts(value) {
92
+ const list = Array.isArray(value) ? value.flatMap((item) => text(item) ?? []) : [];
93
+ return list.length > 0 ? list : undefined;
94
+ }
95
+
96
+ /** 去掉没写的字段:说明会经 IPC 给界面、写进 JSON,不留一串 undefined。 */
97
+ function compact(value) {
98
+ return Object.fromEntries(Object.entries(value).filter(([, field]) => field !== undefined));
99
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * 插件带的原生模块给哪些平台、架构:构建时带上这几份,打包时再按安装包的目标裁掉别的。
3
+ *
4
+ * 预编译的原生模块有两种发法:一个平台一个包、在 package.json 里写 `os`、`cpu`(koffi 的 `@koromix/koffi-<平台>-<架构>`),
5
+ * 或者一个包里放 `prebuilds/<平台>-<架构>/`(node-pty)。插件构建出来的 dist/ 会进安装包、进插件市场,用它的机器
6
+ * 不一定是构建它的那台:Plume 一次打 mac 的 arm64 + x64、Windows 的 x64 + arm64,只带构建机那一份的话,另一个架构的
7
+ * 安装包里终端、沙箱都起不来。所以构建时这几份都带(装了才带:pnpm 的 supportedArchitectures 装齐,仓库外的插件只有
8
+ * 本机那份也照样能构建),打包时 apps/desktop 的 stage-resources.mjs 用 `trimNative` 只留目标那一份。
9
+ */
10
+
11
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ /** Plume 发安装包的平台和架构,按 Node 的 `process.platform`-`process.arch` 写;加上构建机自己。 */
15
+ export const TARGETS = [...new Set([`${process.platform}-${process.arch}`, "darwin-arm64", "darwin-x64", "win32-arm64", "win32-x64"])];
16
+
17
+ /** package.json 的 `os` / `cpu` 这一项容不容得下 value:没写是都行,`!x` 是除了 x,`any` 是都行。 */
18
+ export function fits(list, value) {
19
+ if (!Array.isArray(list) || list.length === 0) return true;
20
+ if (list.includes(`!${value}`)) return false;
21
+ const wanted = list.filter((entry) => !entry.startsWith("!"));
22
+ return wanted.length === 0 || wanted.includes(value) || wanted.includes("any");
23
+ }
24
+
25
+ /** 这个包能不能在 target(`<平台>-<架构>`)上用。 */
26
+ export function forTarget(manifest, target) {
27
+ const [platform, arch] = target.split("-");
28
+ return fits(manifest.os, platform) && fits(manifest.cpu, arch);
29
+ }
30
+
31
+ /**
32
+ * 把一个 node_modules 裁成只给 target 用的:`os`、`cpu` 不合的包整个删掉,`prebuilds/` 下别的平台、架构的目录删掉。
33
+ * 返回删掉了哪些(相对 dir 的路径),给打包日志用。
34
+ */
35
+ export function trimNative(dir, target) {
36
+ const removed = [];
37
+ const walk = (current) => {
38
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
39
+ if (!entry.isDirectory()) continue;
40
+ const path = join(current, entry.name);
41
+ if (entry.name === "prebuilds") {
42
+ for (const prebuilt of readdirSync(path, { withFileTypes: true })) {
43
+ if (!prebuilt.isDirectory() || prebuilt.name === target) continue;
44
+ rmSync(join(path, prebuilt.name), { recursive: true, force: true });
45
+ removed.push(join(path, prebuilt.name).slice(dir.length + 1));
46
+ }
47
+ continue;
48
+ }
49
+ const manifest = join(path, "package.json");
50
+ if (existsSync(manifest) && !forTarget(JSON.parse(readFileSync(manifest, "utf8")), target)) {
51
+ rmSync(path, { recursive: true, force: true });
52
+ removed.push(path.slice(dir.length + 1));
53
+ continue;
54
+ }
55
+ walk(path);
56
+ }
57
+ };
58
+ if (existsSync(dir)) walk(dir);
59
+ return removed;
60
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@plumeai/plugin-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.3",
4
+ "description": "写 Plume 插件用的类型、命令(新建、检查、构建、安装)、插件模板和给 agent 的 skill。随 Plume 应用带一份,也发在 npm 上(插件仓库按版本依赖)。",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/wJiaaa/plume.git",
9
+ "directory": "packages/sdk/plugin-sdk"
10
+ },
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=24"
14
+ },
15
+ "bin": {
16
+ "plume-plugin": "./bin/plume-plugin.mjs"
17
+ },
18
+ "exports": {
19
+ "./tsconfig.json": "./tsconfig.json",
20
+ "./build": "./lib/build.mjs",
21
+ "./lint": "./lib/lint/index.mjs",
22
+ "./manifest": "./lib/manifest.mjs",
23
+ "./targets": "./lib/targets.mjs"
24
+ },
25
+ "dependencies": {
26
+ "@types/node": "^24.10.0",
27
+ "@types/react": "^19.2.0",
28
+ "esbuild": "^0.28.2",
29
+ "lucide-react": "^1.68.0",
30
+ "tailwindcss": "^4.3.4",
31
+ "typescript": "^7.0.2"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,150 @@
1
+ ---
2
+ name: plume-plugin
3
+ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、model.context、step.end、prompt.submit)、`/` 命令、工具卡片、面板、页面、皮肤、逻辑入口和界面入口之间的 serve / remote。新建或修改插件、判断一个需求能不能做成插件时使用。Use when writing, changing or debugging a Plume plugin.
4
+ ---
5
+
6
+ # 写 Plume 插件
7
+
8
+ 插件是一个目录,最多两个入口,都默认导出 `{ name, apply }`,`name` 相同:
9
+
10
+ - **逻辑入口** `src/index.ts` → `dist/index.mjs`:跑在 agent 进程(Node),`apply(ctx)` 里注册工具、往钩子里加层、开接口给界面。
11
+ - **界面入口** `src/ui.tsx` → `dist/ui.mjs`(可选):跑在渲染进程(浏览器,没有 Node),`apply(ui)` 里注册卡片、面板、页面、皮肤。
12
+
13
+ 两个入口一起启用、一起停用。所有注册都记在插件名下,停用或 `apply` 抛错时整体撤销,插件不用自己清理注册。
14
+
15
+ ## 只依赖公开接口
16
+
17
+ 写插件只用这几样,不读、不引用宿主的实现代码:
18
+
19
+ | 能用的 | 用来做什么 |
20
+ | --- | --- |
21
+ | 本 skill | 怎么写、有哪些约束 |
22
+ | 插件 SDK `@plumeai/plugin-sdk` | 类型声明、`plume-plugin` 命令(新建、检查、构建、安装、开发)、tsconfig;下面这些模块名的类型都由它提供。随 Plume 应用发,不经 npm |
23
+ | `@plume/contract`(逻辑入口)、`@plume/contract/ui`(界面入口) | 接口类型,以类型声明里的注释为准;只能 `import type` |
24
+ | `react`、`react/jsx-runtime`、`@plume/ui`、`@plume/styles` | 界面入口运行时和产品共用同一份实例 |
25
+ | `@plume/ui/icons` | 图标,打包进插件,只打用到的 |
26
+ | `node:*`、自己 `package.json` 里声明的第三方包 | 逻辑入口随便用;界面入口只能用浏览器能跑的 |
27
+ | 仓库里的插件(见文末) | 照着写 |
28
+
29
+ 宿主运行时的一切从 `ctx` / `ui` 上拿。SDK 的类型里只有上表这些:`@plume/ui` 只开放 [references/components.md](references/components.md) 里的名字,引用别的类型检查就失败;`plume-plugin check` 还会查 import、色值和 Tailwind 类、写死的中文(Plume 的两个仓库里是 `pnpm check:plugins`,同一套规则)。
30
+
31
+ ## 先判断能不能做成插件
32
+
33
+ 插件能做的:
34
+
35
+ - 给 agent 加工具(`ctx.tools.register`)。
36
+ - 在每次工具调用外面加一层(钩子 `tool.execute`):预先放行、拦截、改参数、改结果、整个接管。
37
+ - 一次操作要问人时先替人决定(钩子 `tool.approve`):放行、拒绝,或交给里层弹审批卡;完全访问、`approved`、「以后不再问」由本体放行、到不了这一层,提权和 `userOnly` 只认人答(层只能拒绝),帮我批准的风险规则是内置插件 plume.approval 在这一层做的。
38
+ - 给自己的工具画审批卡(`ui.approvalCard`):卡片摆在哪、排队、撤卡归产品,卡上画什么、有哪些按钮归插件。
39
+ - 每一轮开始、人的消息写进会话之前看一眼(钩子 `prompt.submit`):拦下这条消息(弹一条原因),或者放行并在它后面给模型补一段上下文。
40
+ - 在每次调模型前改发出去的内容(钩子 `model.context`):按段加、换、删系统提示词,裁剪消息,隐藏工具。只影响这一次请求,会话里存的不变。
41
+ - 每一步收尾时让循环多做一步、给结束换个原因(钩子 `step.end`):以用户身份对模型说一句再接着做,或者以出错、卡住结束这一轮;截断续写、空回复催促、重复提醒和久未汇报的提醒是本体在最里层做的。清单插件经 `StepEnd.progress` 提供汇报判定与提醒文案,本体统一计数;外层可以改写或换掉自然结论,拦不下本来要跑的那一步。
42
+ - 人撤回或编辑重发、要截掉一段对话之前,答那段里自己记着的改动能不能退回,人选了就退回去(钩子 `session.rewind`);截断是本体做的,文件的退回是内置插件 plume.delivery 在这一层做的。
43
+ - 给输入框的 `/` 菜单供命令(`ctx.commands.provide`):列出命令,发送时把 `/名字 参数` 展开成要发的话。
44
+ - 工具卡片可读取 `argumentsText` / `streaming` 绘制安全的静态草稿,成功后经受用户操作校验的 `send(text, event)` 回到卡片所属会话;`ui.captureWindow(rect)` 只能截当前窗口可见区域。用法见 [references/ui.md](references/ui.md)。
45
+ - 在对话里画某个工具的卡片(动态工具名可用 `ui.toolCard` 的 `match: "prefix"`),或者只登记它在转录里的说法(`ui.toolLine`:「查找符号 runAgent」、和读文件归成一类);在工作区加面板(标签按会话保留,按标签归属会话提供 `session` / `cwd`,子会话保留 owner 语义;不能用单例保存当前会话);在侧栏加页面;在输入框底下那排加一格(左边,或右边模型选择前面)、上方项目签那一行加一格;加一张跟着会话的卡片(浮在转录右上角或在输入框正上方);在设置页加一节;换皮肤(只能改 token,`mode` 支持 system/light/dark);经 `ui.ChatBackgroundSettings` 复用产品的背景设置,选图和拖入图片均由宿主校验。
46
+ - 在最新结束的回复下面画卡片(`ui.turnFooter`),随转录滚走,新一轮开始就撤下;每一轮回复底下的操作排里加按钮(`ui.replyAction`);复用产品的高亮差异(`ui.FileDiff`)、文件预览(`ui.FilePreview`)、统一只读文档预览(`ui.DocumentPreview`;经 `ui.documentViewer` 按扩展名注册,读取与权限仍由调用方负责)、文件右键菜单(`ui.useFileMenu`),经 `ui.openFile` 打开指定会话工作目录内的文件。
47
+ - 用 `ui.codeBlock(language, Component)` 接管助手正文的某种围栏代码块;解析、具体 UI、状态和使用说明都归插件,收到原文、会话/消息/块标识、流式与完成状态;可选 `copyText` 在复制回复时提供当前可见值的文字。解析失败返回 `fallback`;宿主在渲染失败或停用时回到源码。完整块里真实点击后可经 `send(text, event)` 回到所属会话,不能在渲染或计时器中发送。
48
+ - 用 `ui.mentions` 给 `@` 菜单提供当前目录可用的智能体或技能引用;选中只插入 token,不自动发送。页面角标 `ui.pageBadge` 可带 `attention: true` 同时进入通知菜单。
49
+ - 给输入框一句灰字建议,人按 Tab 填进去(`ui.composerHint`);画回复前拿掉插件让模型顺带写的标记(`ui.replyText`,只改显示和复制);改会话标题(`ctx.sessions.setTitle`)。
50
+ - 用设置里的生图模型画图、改图(`ctx.model.findImageModel`、`ctx.model.image`),图存进会话媒体目录(`ctx.media`);界面入口显示(`ui.mediaUrl`)、用产品的看图器看和标注(`ui.openImage`),另存、复制(`ui.saveImage`、`ui.copyImage`),把图带回某个会话的输入框(`ui.draftInSession`),跳到某次工具调用的卡片(`ui.revealToolCall`);媒体图可在文件面板打开或在系统文件管理器定位(`ui.openMediaFile`、`ui.revealMediaFile`,菜单名取 `ui.revealMediaLabel()`)。
51
+ - 侧栏会话行首空闲时画自己的记号(`ui.sessionMark`);删除确认框里补一句会连带、会留下什么(`ui.deleteNote`);人点了按钮时归档会话(`ctx.sessions.setArchived`);模型照人的要求归档它所在的对话时,工具那次调用经 `run.archiveAfterTurn()` 交给本体,这一轮以完成收场后才归档(只有顶层会话有)。
52
+ - 弹提示条(界面入口 `ui.notify`,逻辑入口 `ctx.notice`)、打开、关闭自己的面板(`ui.openPanel`、`ui.closePanel`)、把一段话写进输入框等人自己发(`ui.draftInSession`,`replace` 时整段换掉),或在项目签那一行的格子里,人点了写明要发什么的按钮时以人的身份直接发(`send`);逻辑入口用默认模型做一次性的小调用(`ctx.model.complete`)、读会话(`ctx.sessions`)、读模型调用的花销(`ctx.model.usage`)。一次性结果要覆盖持久化内容时用 `ModelCompletion.requireComplete` 拒收截断结果;经 `ctx.sessions.onContextReset` 在压缩、撤回或上下文来源变化时作废自己的缓存。
53
+ - 人点了明确的按钮时,经 `ui.openConversationDraft({ project?, cwd?, text })` 打开项目或目录的空白对话并准备草稿,保留已有草稿、不自动发送;经 `ui.openSettings(id)` 打开自己占导航一行(`page: "nav"`)的设置分区,经 `ui.openExternal(url)` 在系统浏览器打开不带凭据的 HTTP(S) 链接。
54
+ - 开一个新的顶层会话并发出第一句话(`ctx.sessions.launch`,定时任务到点了、人点了「在新会话里执行」):它进侧栏、不归插件,人接着聊,插件之后碰不到它;`done` 是第一轮怎么结束的,`idle` 在它第一次真闲下来时兑现(后台运行都回来、问出去的请求都有了答复、唤起的几轮也做完),`reportBack(text, status)` 把结果交回开出它的会话一次(`idle` 兑现时带着最后一轮的收场和最后一段回复,交回用它);`spawnedFrom` 记着从哪个会话、哪次调用(`run.toolCallId`)开出来,要交回时那次调用得是你自己的工具在那个会话里的调用,不然 `reportBack` 抛错;人点了按钮时经 `ui.openSession(id)` 打开它。页面那一行挂数目(`ui.pageBadge`),别处的按钮经 `ui.openPage(id)` 带人去自己的页面。
55
+ - 建自己的子会话并发送消息(`ctx.sessions.create` / `send`),或经 `start` / `wait` 在后台跑、经 `report` 把该次结果送给真实父会话;`steer` 在运行中插话,`onStopped` 撤销尚未启动的排队工作。停止父会话后后台结果不能再唤醒它。自己的子会话里,自己的工具那次调用拿得到父会话(`run.parent`:`enqueue` 排进它的队列、`stop` 停下、`carryOn` 接着做、`status`),工具卡片上人点的按钮经 `handToParent` 把一件事排进父会话;作用对象只有这个父会话,写进去的话标着来自侧边聊天、不算人的授权。`models` 只给无密钥的模型目录,建子会话时可选模型和思考档位。
56
+ - 给自己的页面建一个一直在的助手:`ctx.sessions.create` 不给 `parentId` 就是插件的独立会话(不挂在任何会话下、不进侧栏,只有你能发),界面经 `ui.Conversation` 画它,审批卡就压在这个视图的输入框上方。
57
+ - 界面经 `ui.Conversation`、`ui.useSession` 复用转录与实时状态;可局部定制任务消息、去掉回报重复正文,操控输入框用 `mode: "steer"`。`ui.Markdown` 复用正文和文件链接;`ui.composerAbove` 在主输入框上方独占一行。经 `ui.quoteAction` 接主会话划词菜单,`ui.quoteInPanel` 把引用追加到自己的面板草稿;`composer.draftKey` 按标签保存文字、附件和引用。`/` 命令可以自己办掉、不发消息(`{ handled: true }`)。
58
+ - 界面经 `ui.onSessionsChanged` 订阅已保存消息、摘要和删除事件,刷新画廊等页面;经 `ui.ComposerSend` 复用发送/停止按钮,人真实点击停止时调用 `ui.stopSession`。终端插件经 `ui.terminalOpener` 承接文件树等入口的目录打开请求,经 `ui.terminalRunner` 承接代码块的「在终端运行」,经 `ui.findFile` / `ui.openFile` 打开输出里的路径、`ui.openLink` 打开网址、`ui.droppedPaths` 读拖进来的文件路径。
59
+ - 截屏:经 `ui.screen` 开盖住屏幕的浮层、注册全局快捷键,浮层和钉在桌面上的小窗口里画自己的组件。
60
+ - 在逻辑入口跑长期的东西(子进程、定时器、文件),经 `ctx.serve` / `ui.remote()` 给自己的界面用;HTTP(S) 请求可用 `ctx.fetch(url, init)` 继承系统代理,响应兼容标准 Fetch,禁止 URL 凭据、自动重定向和共享 cookie,插件停用自动取消。
61
+ - 画网页:界面入口在面板里放 `<webview>`,逻辑入口经 `ctx.pages` 驱动它(加载、读、点按、输入、截图、缩放、视口、应答对话框);`ctx.pages.inspect(url, options)` 可在独立临时沙箱中检查页面,返回异常、控制台错误、高度和可选分段截图,不需要挂面板。面板设 `keepAlive`,标签在统一扁平常驻层,换会话、去设置页都不重挂,后台仍收到归属会话;`shown` 按是否正在显示所属会话的当前标签计算,`available` 按目录和有无会话分别计算、只控制新开列表,不卸载已有标签;设 `detachable`,弹出去的窗口里仍重新挂上。工具结果带图(`images`)给模型看;把字连同图片附件接进输入框(`ui.draftInSession`,不给会话就是眼前的输入框);回复里的网址、文件签多一种打开方式(`ui.linkOpener`);主输入框排队条上,排着的消息「更多」菜单里多一项、把这条拿到别处去发(`ui.queueAction`)。
62
+ - 工具答完以后在后台接着跑的事,结束或有新进展时经 `run.background()` 的渠道送回这个会话(`report`,闲时开一轮、忙时在步骤边界读入,人按停后作废;只有顶层会话有)。
63
+ - 会话之间递话:顶层会话里,工具那次调用经 `run.letter(to, letter)` 给另一个顶层会话寄一封信(请求、答复或通知,本体照实填发件方、在前面说明不是用户的话),回执的 `read` 告诉你读到没有,`answer` 能替收件方回发件方一次(代答)。`run.letter`、`run.parent` 只在这次调用里能用,调用答完再用就抛错。
64
+ - 读磁盘文件前可用 `file.read` 服务的 `authorize(run, path)`,与内置文件工具共用项目目录、符号链接和密钥审批规则,只授予读取;通过 `inject` 声明依赖,成功后使用返回的绝对路径。
65
+ - 把只为这次答案写的文件放进会话的临时目录(`run.scratchDir`,会话删掉时一起删):太大不值得交给模型的图写在这里、`output` 给路径、图放进结果的 `shownImages` 只给人看;经服务 `resources` 加一种资源地址(`scheme://路径`,模型用 read、ls 读它),或把大块原文存成 `artifact://` 地址(`keepArtifact`)。
66
+
67
+ 插件做不到的:替换模型调用或 agent 循环、换掉别的工具(本体的、别的插件的)的审批卡、改模型回复(存下的那份;显示时拿掉标记见 `ui.replyText`)、在没有用户停止操作时让本来要接着做的一轮停下(`step.end` 只能多做或给结束换原因,`ui.stopSession` 只接受真实点击)、任意往会话里写东西或发起一轮(只允许自己的子会话和独立会话、钩子 `say` / `context`、宿主校验过的后台运行结果 `report`、工具经 `run.background()` 送回本会话的后台结果、工具经 `run.letter` 寄给另一个顶层会话的信(标着来自哪个会话)、`launch` 开出来的新会话的第一句话,以及自己的子会话里经 `run.parent` / `handToParent` 排进它父会话的任务;不能往别的已有会话里发、不能指定任意父会话或冒充用户授权)、没人点按钮时替人发送、绕过公开草稿接口操纵输入框、新增钩子或界面扩展点。
68
+
69
+ **需求要用到做不到的能力时,停下来报告缺的是哪个能力、哪个插件需要它**,这是宿主的改动,不在插件里绕过(不改宿主代码、不碰宿主内部对象、不往页面上自己挂 DOM 冒充扩展点)。
70
+
71
+ ## 流程
72
+
73
+ 1. **定名字**:插件名小写加点分段(Plume 自己提供的插件都以 `plume.` 开头);目录名只用小写字母、数字和连字符,它也是 `plume-plugin://` 的主机名。
74
+ 2. **搭目录**:`plume-plugin init <目录>` 从模板生成。命令在 `~/.plume/bin/plume-plugin`(Windows 的 cmd、PowerShell 里是同目录的 `plume-plugin.cmd`;Plume 启动过一次就有,用 Plume 自己的运行时跑,不用装 Node 和 npm);Plume 的插件仓库(plume-plugins)里用 `pnpm plugin init plugins/<目录名>`(见下面的骨架),模板里是一个能跑的工具加卡片,改成自己的。
75
+ 3. **逻辑入口**:读 [references/logic.md](references/logic.md)。
76
+ 4. **界面入口**(要画东西时):读 [references/ui.md](references/ui.md)。
77
+ 5. **样式和文案**(有界面或给人看的文字时):读 [references/style-i18n.md](references/style-i18n.md)。
78
+ 6. **构建和加载**:
79
+ - 独立项目:`plume-plugin dev` 构建、装进 `~/.plume/plugins/<目录名>/`,之后一改源码就重新构建、装上;Plume 开着的话每次都当场加载,不用重启。只装一次用 `plume-plugin build` 再 `plume-plugin install`。也可以把插件的上级目录放进环境变量 `PLUME_PLUGIN_PATHS`,Plume 同样盯着那里。
80
+ - Plume 的插件仓库里(`plugins/<目录名>`):`pnpm plugin dev plugins/<目录名>`,和独立项目一样构建、装进本机的 Plume;要连市场一起试(安装、更新),`pnpm market` 起一个本机市场,Plume 启动前设它打印的 `PLUME_MARKET_URL`。
81
+ 7. **自检**:对照文末清单,再跑检查(独立项目 `plume-plugin check`:类型检查加插件规则;仓库里 `pnpm check`)。
82
+
83
+ ## 目录骨架
84
+
85
+ `plume-plugin init <目录> [--name 插件名] [--display 市场里的名字]` 生成:
86
+
87
+ ```
88
+ <目录名>/
89
+ ├── package.json # plume.displayName、plume.category 是「插件市场」里的名字和分类;仓库里还写 @plumeai/plugin-sdk 等开发依赖
90
+ ├── tsconfig.json # 继承 @plumeai/plugin-sdk/tsconfig.json
91
+ ├── README.md # 「插件市场」里的介绍:它做了什么、怎么用
92
+ ├── images/ # 可选:README 引用的图片(相对路径 `images/xx.png`),随包分发
93
+ ├── icon.png # 可选:插件图标,市场、详情和设置页共用,随安装包分发
94
+ ├── AGENTS.md、CLAUDE.md、.claude/skills/plume-plugin/ # 只有独立项目有:给接手的 agent
95
+ ├── node_modules/ # 只有独立项目有:init 建的链接,指到随 Plume 发的 SDK;坏了跑 plume-plugin link
96
+ ├── skills/ # 可选:随包的技能,`skills/<名字>/SKILL.md`;装了「技能」插件时进技能目录,和 dist/ 一起装
97
+ └── src/
98
+ ├── index.ts # 逻辑入口(必需)
99
+ ├── ui.tsx # 界面入口(可选)
100
+ ├── messages.ts # 给人看的文字,中英两份
101
+ └── types.ts # details 的类型和校验,两个入口共用
102
+ ```
103
+
104
+ 用 serve / remote 时再加 `src/api.ts`,放两个入口共用的接口类型。
105
+
106
+ - `package.json` 的 `description`、`keywords`、`plume.displayName`、`plume.category` 填好,「插件市场」里显示它们。
107
+ - `plume.defaultPrompt` 可选,几句示例提问(字符串数组,最多显示四句):插件装上并开着时,详情页「试试这样问」列出它们,点一句开新对话、写进输入框。
108
+ - README 里的图片:插件目录里的写相对路径(放在 `images/` 下,支持 PNG、JPEG、GIF、WebP、SVG,每张不超过 5 MiB),网上的写 https 地址;带深色版本的 `<picture>` 按窗口明暗选。
109
+ - 根目录的 `icon.png` 会自动作为插件图标,不用配置;使用正方形 PNG,建议 192×192,文件不超过 1 MiB。不提供或图片损坏时显示默认标记。
110
+ - `devDependencies` 只用于类型和打包:逻辑入口用到的第三方包全部打进 `dist/index.mjs`。
111
+ - `dependencies` 只放打不进去的原生模块(如 `node-pty`):构建时连同实际安装的运行依赖、可选依赖和 peer 依赖一起复制进 `dist/node_modules`,只保留 Plume 发包的平台、架构(darwin、win32 的 arm64、x64,加上构建机)的包与预编译文件,随 `dist/` 一起装。
112
+ - 引入 `.css` 时加一个 `src/css.d.ts`(写法见 style-i18n.md)。
113
+
114
+ ## 自检清单
115
+
116
+ - [ ] 两个入口的 `name` 相同,`satisfies Plugin` / `satisfies UiPlugin`。
117
+ - [ ] 只 `import type` `@plume/contract`;没有引用公开接口以外的东西。
118
+ - [ ] 工具结果里给模型的写进 `output`,给卡片的写进 `details`;卡片先校验 `details`,认不出就返回 `fallback`。
119
+ - [ ] 钩子的层:不需要处理的调用原样 `next(input)`;`order` 选得对(见 logic.md);出错直接抛,不吞。
120
+ - [ ] 逻辑入口自己起的定时器、子进程、监听登记了 `ctx.effect`;界面入口在 apply 中启动的订阅、远端 watch 登记 `ui.effect` 清理(如 `ui.effect(() => call.cancel())`),停用、重载或 apply 失败时都会执行。
121
+ - [ ] 界面:没写 Tailwind 类;文字用 `Text`,组件只用 [references/components.md](references/components.md) 里列的;自己的 CSS 只管布局,颜色只取 `var(--plume-*)`;浅色、深色都能看。
122
+ - [ ] 给人看的文字中英两份;发给模型的文字和日志标了 `i18n-exempt`。
123
+ - [ ] `README.md` 写了它做什么、怎么用。
124
+ - [ ] 类型检查通过(仓库里 `pnpm check` 通过);改了界面或行为的,装进 Plume 实际跑一遍。
125
+
126
+ ## 可以照着写的插件
127
+
128
+ 都在 Plume 插件仓库(plume-plugins)的 `plugins/` 下,`todo`、`commands`、`web`、`skills`、`git` 随应用发、在本体仓库的 `packages/builtin/` 下(拿不到仓库时,以模板和本 skill 里的代码片段为准):
129
+
130
+ | 插件 | 看它的什么 |
131
+ | --- | --- |
132
+ | `todo` | 注册工具、details 让本体的转录认得(不注册卡片,调用并进一段活);`step.end` 在本体的结论外面追问一句、工具结果带 `terminate` 结束这一轮、`run.calls` 看同一步的其他调用;跟着会话的卡片(`ui.conversationCard`)从会话的消息里读最新一份、按 `placement` 换外观、按钮经 `stop` / `carryOn` 停下或接着做;插件自带样式表;`messages.ts` 的写法 |
133
+ | `terminal` | `ctx.serve` / `ui.remote()` 流式输出和取消、原生依赖、`ctx.effect`、多标签面板、插件自带样式表、点链接打开文件和网址、拖文件进来粘贴路径 |
134
+ | `scheduled` | `ctx.dataDir` 存数据、页面(`ui.page`)经 `ui.remote()` 读写;逻辑入口常驻定时器按时 `ctx.sessions.launch` 开新会话、等第一轮结束判失败;`watch` 推状态,界面入口一直连着、给页面挂数目(`ui.pageBadge`)、在输入框上方说失败(`ui.composerAbove`、`ui.openPage`),卡片上「打开上次会话」(`ui.openSession`) |
135
+ | `git` | 大一点的插件:逻辑入口跑子进程、`ctx.model.complete` 写提交说明;面板里多个视图、`ui.remote()` 的 `.cancel()` 叫停远端调用;项目签那一行加格子(`ui.composerChip`)、`ui.notify`、`ui.openPanel`、`ui.draftInSession`;diff 评论攒起来经 `ui.draftInSession` 的 `send` 交给面板所在的会话;几个样式表一起挂;`ctx.provide` 提供服务 `session.worktree`;对话自己的工作树:`prompt.submit` 的 `cwd` 在第一轮把会话挪进去,`ctx.sessions.onDeleted`、`onArchived` 收回、检出回来,项目签那一格的 `send`、`ui.sessionMark`、`ui.deleteNote`、设置分区 |
136
+ | `memory` | `model.context` 按段插提示词、每个会话冻结一份;按条件对模型藏起工具;`ctx.sessions` 读旧会话、`ctx.model.complete` 在后台定时总结;设置分区画进「个性化」页(`page`),「智能体」页「会话」那张卡里一行选总结用的模型(`ui.ModelSelect`) |
137
+ | `usage` | 设置 › 数据里的一页(`ui.settingsSection` 的 `page: "nav"` 加 `group: "data"`,页头介绍用 `detail`);逻辑入口经 `ctx.model.usage` 读花销、`ctx.serve` 转给界面;SVG 自己画的堆叠柱状图(悬停气泡 fixed 定位)、环图(`stroke-dasharray` 分段)、热力图和色点,插件自带的模型品牌标 |
138
+ | `screenshot` | 截屏浮层(`ui.screen`、`ui.screenOverlay`、`ui.pinnedShot`)、标注组件、输入框按钮(`ui.composerItem`)、占导航一行的设置分区(`ui.settingsSection` 的 `page: "nav"`)、逻辑入口跑子进程 |
139
+ | `commands` | `ctx.commands.provide` 供 `/` 命令(每次现读磁盘);设置分区经 `ui.remote()` 新建、打开、删除文件 |
140
+ | `hooks` | 四个钩子一起用:`prompt.submit` 拦消息、补上下文,`tool.execute` 预先放行、要确认(`input.ask`)、改参数、接结果,`tool.approve`(order 2000)替人答审批,`step.end` 让它接着干;`ctx.sessions.observe` 的 `turnEnd` 和 `ctx.sessions.onEnded` 接一轮失败、会话结束;逻辑入口跑子进程,项目钩子读 `.plume/config.json` 的 `hooks`、按内容指纹信任;运行记录存在 `ctx.dataDir`、`watch` 推变化,`ui.replyAction` 在回复底下看、`ui.composerAbove` 提示待审核、`ui.openSettings` 去设置分区;设置分区里 `ui.useProjects` 切范围、表单弹窗 |
141
+ | `im` | 不给 `page` 的设置分区:画进插件详情的「设置」一节,不占设置导航;密钥经 `ui.remote()` 交给逻辑入口存进 `ctx.dataDir`,界面只知道存没存 |
142
+ | `index` | `inject` 服务 `project.files` 列项目文件;按会话缓存工具要用的数据、会话删掉时丢掉;设置分区用 `ui.useProjects` 列项目、经 `ui.remote()` 看状态、触发重建 |
143
+ | `web` | 只读的工具(不弹审批卡)、`ctx.dataDir` 存 API key、key 不经界面(界面只知道填没填) |
144
+ | `mcp` | 运行中途增减工具:`ctx.tools.register` 返回的函数撤下单个工具;逻辑入口连外部服务、调用前 `run.ask`;大图存进 `ctx.media`、放进结果的 `shownImages` 由通用卡片画;服务要人填表时 `run.ask` 带 `userOnly` 和一句 `waiting`;`model.context` 按估算的大小藏起工具、换成搜索入口;`ctx.sessions.onDeleted` 关按会话起的连接;`ctx.dataDir` 存配置和令牌,密钥不经界面;设置分区里的卡片(就地编辑、失焦保存)、弹窗和 `Popover` 菜单 |
145
+ | `skills` | `model.context` 按磁盘现状加一段、没东西时藏起自己的工具;`tool.execute` 的一层按会话状态拒掉调用;`prompt.submit` 的 `context` 给模型补一句;`ctx.commands.provide` 把技能放进 `/` 菜单、`displayText` 只给人看;设置分区里用 `ui.FileDiff` 画差异 |
146
+ | `subagents` | `start` / `wait` / `report` 后台派发和回报,按原子会话续跑,`steer` 插话;一个工具名、参数按会话换(`model.context` 改 `yield` 的参数为定义的输出结构),结果带 `terminate` 收尾,`step.end`(order -20)在父会话里有子代理没回来时经 `awaiting` 告诉里层别逼它交结论、每 4 次带工具的请求提醒一次在等谁,在子会话里每 60 步一个检查点,读子会话里最新的 `todo_write` 结果决定接着跑、`say` 一句让它列清单或讨交接(`stepsLeft` 只剩两步时也讨),讨交接的两步 `tool.execute` 拒掉别的工具、第二步的结果带 `terminate`;插件管理种类、名额和排队,停止监听清队列,子会话收工时经 `inject: ["background.jobs"]` 停掉它起的后台进程;`composerAbove`、任务卡和回报卡,`ui.Conversation` 局部渲染与附件操控;「智能体」整页(`page: "agents"`):读写定义文件、`ui.useProjects` 切范围、`ctx.tools.names` 挑工具、`ui.ModelSelect` 选模型、插件自己的选脸弹层 |
147
+ | `plan` | 按会话记的开关(`ui.composerItem` 里的 `ComposerToggle`、`/plan`,新对话的开关经 `prompt.submit` 交给建出来的会话);`tool.execute` 拒绝写文件、其余调用设 `readOnly`;工具经 `run.ask` 带 `userOnly` 和 `waiting`(「等你批准计划」)问人,自己画审批卡(`ui.approvalCard`),附言经 `remote` 先交给逻辑入口;面板里划线评论(CSS Custom Highlight);`context.compact` 往摘要后面接上批准的计划;`model.context` 加一段、`step.end` 提醒一次;`submit_plan` 经 `ui.toolLine` 在转录里读成一行,环境卡片里一节(`ui.environmentSection`)列出批准过的计划、点了打开面板,面板从会话消息里读 |
148
+ | `browser` | 网页:界面入口画 `<webview>`、挂上后把 id 交给逻辑入口,逻辑入口经 `ctx.pages` 驱动;面板 `keepAlive` + `detachable`,只给最近看过的几个会话挂页面、agent 要用时叫醒;逻辑入口经 `watch` 推来「开、关哪个标签」,界面用 `ui.openPanel` 的 `tab` / `background` 和 `ui.closePanel` 的 `tab` 照办;`browser_screenshot` 的结果带图;选取卡经 `ui.draftInSession` 带上截图;`ui.linkOpener` 接回复里的网址和 `.html`;逻辑入口起本机小服务开本地 `.html`;工具在调用期间接管页面的对话框;`ctx.dataDir` 存设置和书签;随包的 `skills/` |
149
+ | `side-chat` | 面板里的多段子会话,`ui.Conversation` 带输入框;`prompt.submit` 给每一问附上主会话的增量,主会话的下一问附上侧边聊天改过的文件(`inject: ["file.changes"]` 认工具结果上的改动记录);`/btw` 自己办掉(`handled`、`openPanel`);排队消息的「在侧边聊天中打开」(`ui.queueAction`);关标签前用 `Confirm` 问一句;「会话」卡里一行选新侧边聊天的模型(`page: "session-models"`) |
150
+ | `image` | 用设置里的生图模型出图(`ctx.model.image`),图存进媒体目录(`ctx.media`);工具卡片经进度推 JSON 行、边画边显示,`ui.mediaUrl` 显示、`ui.openImage` 看图和标注、`ui.draftInSession` 把图带回会话;`model.context` 没有生图模型时藏起工具;画廊页(`ui.page`)经 `ui.remote()` 扫全部会话、`ui.revealToolCall` 跳回那次调用 |