@plumeai/plugin-sdk 0.2.0 → 0.3.0

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 (31) hide show
  1. package/dist/client/ui/src/avatar/AgentAvatar.d.ts +2 -5
  2. package/dist/client/ui/src/i18n/messages/en.d.ts +3 -0
  3. package/dist/client/ui/src/i18n/messages/zh-CN.d.ts +3 -0
  4. package/dist/client/ui/src/image/AnnotateToolbar.d.ts +2 -4
  5. package/dist/client/ui/src/image/Annotator.d.ts +3 -2
  6. package/dist/client/ui/src/image/screenshot-geometry.d.ts +2 -2
  7. package/dist/client/ui/src/inputs/SearchField.d.ts +1 -1
  8. package/dist/client/ui/src/inputs/pickers.d.ts +1 -1
  9. package/dist/client/ui/src/layout/PageTopRow.d.ts +5 -6
  10. package/dist/client/ui/src/motion/RollingText.d.ts +1 -1
  11. package/dist/client/ui/src/motion/loaders.d.ts +4 -4
  12. package/dist/client/ui/src/overlay/Confirm.d.ts +1 -2
  13. package/dist/client/ui/src/overlay/ContextMenu.d.ts +2 -2
  14. package/dist/client/ui/src/overlay/Dialog.d.ts +5 -6
  15. package/dist/client/ui/src/overlay/Menu.d.ts +3 -4
  16. package/dist/client/ui/src/overlay/Popover.d.ts +1 -1
  17. package/dist/client/ui/src/overlay/Tooltip.d.ts +1 -1
  18. package/dist/client/ui/src/primitives/IconButton.d.ts +2 -2
  19. package/dist/client/ui/src/primitives/Text.d.ts +2 -2
  20. package/dist/client/ui/src/scroll/ScrollText.d.ts +1 -1
  21. package/dist/client/ui/src/scroll/Scroller.d.ts +2 -1
  22. package/dist/contract/src/diagnostics.d.ts +39 -1
  23. package/dist/contract/src/plugin.d.ts +70 -1
  24. package/dist/contract/src/service.d.ts +15 -2
  25. package/dist/contract/src/ui.d.ts +73 -69
  26. package/package.json +1 -1
  27. package/skill/SKILL.md +86 -78
  28. package/skill/references/components.md +233 -1418
  29. package/skill/references/logic.md +240 -74
  30. package/skill/references/style-i18n.md +16 -12
  31. package/skill/references/ui.md +171 -124
@@ -5,7 +5,7 @@ import type { Remote } from "../../infra/rpc/src/index.d.ts";
5
5
  import type { ComponentType, MouseEvent, ReactNode } from "react";
6
6
  import type { ChangeHunk } from "./changes.ts";
7
7
  import type { Locale, SlashCommand, ToolCall, ToolOutput } from "./plugin.ts";
8
- import type { ApprovalDecision, ApprovalRequest, ChildTask, FileContents, HumanRequest, OutgoingMessage, PermissionMode, SessionSummary, SessionTarget, SessionView, ThinkingLevel } from "./service.ts";
8
+ import type { ChildTask, FileContents, HumanRequest, OutgoingMessage, PermissionMode, SessionSummary, SessionTarget, SessionView, ThinkingLevel } from "./service.ts";
9
9
  import type { AssistantMessage, DeliveredReport, UserMessage } from "../../core/llm/types.d.ts";
10
10
  export type { Remote, SessionView, ToolCall, ToolOutput };
11
11
  /** 回复正文中的围栏代码块;语法与内容由插件校验,宿主不执行其中的代码。 */
@@ -243,7 +243,7 @@ export interface PanelOptions {
243
243
  }>;
244
244
  /**
245
245
  * 打开这个面板的快捷键,Electron accelerator 的写法(`Control+\``、`CommandOrControl+Shift+P`),卡片上会显示它。
246
- * 作用于当前会话:面板区没开或在看别的就切到它(没开着就新开一个标签),正在看它就收起。和别的面板重了归后注册的,它撤下后还给前一个;Plume 自己的快捷键(⌘B、后退前进、Esc)和它自己面板的快捷键拿不走。
246
+ * 作用于当前会话:面板区没开或在看别的就切到它(没开着就新开一个标签),正在看它就收起。和别的面板重了归后注册的,它撤下后还给前一个;Plume 自己的快捷键(⌘B、后退前进、Esc,本体文件树的 ⌘P、文件面板的 ⌥⌘P)拿不走。
247
247
  */
248
248
  shortcut?: string;
249
249
  order?: number;
@@ -357,36 +357,10 @@ export interface ChildTasksProps {
357
357
  /** 收了场的那件从列表里拿掉,见 `AgentService.dismissTask`。 */
358
358
  dismiss(taskId: string): void;
359
359
  }
360
- /**
361
- * 插件给自己的工具画的审批卡(`ui.approvalCard`)。卡片压在输入框正上方的那一格由产品摆,排队、停下这一轮时撤卡也归产品;
362
- * 组件画整张卡(外框、正文、按钮),人拍板时调 `respond`。要人给答案(提问、计划、表单)的不是审批,用 `run.request` 和
363
- * `ui.requestCard`(`RequestCardProps`)。
364
- */
365
- export interface ApprovalCardProps {
366
- request: ApprovalRequest;
367
- /** 在问的会话;子会话(侧边聊天、子 Agent)问时是子会话的 id。 */
368
- sessionId: string;
369
- /** 问的是子会话时它的标题,卡上要写明是谁在请求。 */
370
- from?: string;
371
- /** 排在这张后面还在等的审批张数,卡上写 +N(`@plume/ui` 的 `ApprovalFrame` 替你写)。到期时刻在 `request.expiresAt`。 */
372
- queued: number;
373
- /** 交出回答。没送到时抛错,卡片自己把错误摆出来、让人再答。 */
374
- respond(decision: ApprovalDecision): Promise<void>;
375
- /** 设置里现在的权限模式。 */
376
- permission: PermissionMode;
377
- /**
378
- * 把权限模式从请求批准切到帮我批准,给「接受并开启自动模式」这样的按钮:先切,再 `respond`。
379
- * 只在人点了卡上的按钮时调;不在请求批准档时什么都不改(不会把完全访问降下来)。
380
- */
381
- allowAuto(): Promise<void>;
382
- /** 窄窗口:面板区一次只摆一格,打开面板会盖住这张卡,要读的东西画在卡里。 */
383
- compact: boolean;
384
- /** 产品的通用审批卡。认不出这次请求时直接返回它。 */
385
- fallback: ReactNode;
386
- }
387
360
  /**
388
361
  * 插件给 `run.request` 画的卡(`ui.requestCard`):问题、计划、表单。摆在哪、排队、过期、停下这一轮时撤卡归产品,
389
362
  * 组件画整张卡(外框用 `@plume/ui` 的 `ApprovalFrame`),人答了调 `answer`,不答调 `decline`。
363
+ * 审批(放不放行)的卡只有产品的那一张,插件换不掉。
390
364
  */
391
365
  export interface RequestCardProps {
392
366
  request: HumanRequest;
@@ -402,7 +376,10 @@ export interface RequestCardProps {
402
376
  decline(): Promise<void>;
403
377
  /** 设置里现在的权限模式。 */
404
378
  permission: PermissionMode;
405
- /** 同 `ApprovalCardProps.allowAuto`:「接受并开启自动模式」这样的按钮先调它,再 `answer`。 */
379
+ /**
380
+ * 把权限模式从请求批准切到帮我批准,给「接受并开启自动模式」这样的按钮:先调它,再 `answer`。
381
+ * 只在人点了卡上的按钮时调;不在请求批准档时什么都不改(不会把完全访问降下来)。
382
+ */
406
383
  allowAuto(): Promise<void>;
407
384
  /** 窄窗口:面板区一次只摆一格,打开面板会盖住这张卡,要读的东西画在卡里。 */
408
385
  compact: boolean;
@@ -786,15 +763,34 @@ export interface DraftQuote {
786
763
  text: string;
787
764
  from: "user" | "assistant";
788
765
  }
766
+ /**
767
+ * 人在转录里划选一段字后,引用菜单里多的一项,见 `UiContext.quoteAction`。宿主只在人点了那一项时调 `run`;
768
+ * `quote` 是划选的那段,`sessionId` 是它所在的会话。拿去哪里由插件定(`ui.quoteInPanel` 放进自己面板的输入框)。
769
+ */
789
770
  export interface QuoteAction {
790
771
  label: LocalizedText;
791
772
  run(quote: DraftQuote, sessionId: string): void | Promise<void>;
792
773
  }
774
+ /** `ui.onSessionsChanged` 收到的一次变化:只说哪个会话、哪一类变了,内容要自己重读。 */
793
775
  export interface SessionChange {
794
776
  sessionId: string;
777
+ /** `messages`:消息落盘或撤回;`summary`:摘要(标题、运行状态……)变了,新会话也算;`deleted`:删掉了。 */
795
778
  type: "messages" | "summary" | "deleted";
779
+ /** 落盘的是工具结果时带上那件工具的名字,别的消息不带。 */
796
780
  tools?: string[];
797
781
  }
782
+ /** `ui.ComposerSend` 收的。 */
783
+ export interface ComposerSendProps {
784
+ /** 停止键停的会话。 */
785
+ sessionId: string;
786
+ /** 这个会话正在跑:画成停止键,按下停下它这一轮。 */
787
+ running: boolean;
788
+ /** 只禁用发送;跑着时停止键一直能按。 */
789
+ disabled?: boolean;
790
+ /** 发送键的悬停说明,不给是「发送」。 */
791
+ tip?: string;
792
+ onSend(): void;
793
+ }
798
794
  /** 欢迎页问候下面那一行(`ui.welcomeLine`):一句话,可以带出处。 */
799
795
  export interface WelcomeLine {
800
796
  text: string;
@@ -807,34 +803,18 @@ export interface ComposerMention {
807
803
  title: string;
808
804
  description?: string;
809
805
  origin?: string;
810
- kind: "subagent" | "plugin";
806
+ /**
807
+ * 宿主怎么画它:`agent` 是一个能点名叫来干活的智能体,归进「智能体」一组,行首和输入框里的 token 画它的头像(`avatar`);
808
+ * `plugin` 归进「插件和技能」一组,行首是插件图标。只管画法和分组,选中照样只插入 `token`。
809
+ */
810
+ kind: "agent" | "plugin";
811
811
  token: string;
812
+ /** 只给 `agent`:写明的头像,写法同 `@plume/ui` 的 `assignAvatars` 收的 `avatar`;不给按名字算一个,同一份名单里互不相同。 */
812
813
  avatar?: string;
813
814
  }
814
815
  export interface UiContext {
815
- quoteAction(action: QuoteAction): void;
816
- /** 追加到当前可见的同插件面板,或开新标签;保留草稿,不自动发送。 */
817
- quoteInPanel(panelId: string, sessionId: string, quote: DraftQuote): void;
818
- /** 仅接受当前真实点击,与主输入框的停止相同。 */
819
- stopSession(sessionId: string, event: MouseEvent): Promise<void>;
820
- onSessionsChanged(listener: (event: SessionChange) => void): () => void;
821
- openMediaFile(name: string, sessionId: string): Promise<void>;
822
- revealMediaFile(name: string): Promise<void>;
823
- revealMediaLabel(): string;
824
- ComposerSend: ComponentType<{
825
- running: boolean;
826
- disabled?: boolean;
827
- tip?: string;
828
- onSend(): void;
829
- onStop(event: MouseEvent<HTMLButtonElement>): void;
830
- }>;
831
- /** 文件树等入口打开终端;处理器只在用户操作时运行。 */
832
- terminalOpener(open: (directory: string, sessionId: string) => void | Promise<void>): void;
833
- /**
834
- * 回复里 shell 代码块的「在终端运行」:人点击后调用 `run(command, sessionId)`,`command` 已去掉提示符(`$ `、`> `)和注释行,
835
- * 要原样执行;`sessionId` 是代码块所在的会话。登记和没有处理器时的提示同 `terminalOpener`。
836
- */
837
- terminalRunner(run: (command: string, sessionId: string) => void | Promise<void>): void;
816
+ /** 插件名,和逻辑入口的一样。 */
817
+ readonly name: string;
838
818
  /** 会话标题栏的局部控件;只在有会话时显示,组件不能接管标题或布局。 */
839
819
  sessionToolbar(id: string, component: ComponentType<{
840
820
  session: SessionView;
@@ -867,7 +847,6 @@ export interface UiContext {
867
847
  readonly ProjectOpener: ComponentType<{
868
848
  path: string;
869
849
  }>;
870
- readonly name: string;
871
850
  /**
872
851
  * 人明确选择入口时在主窗口打开一个会话,和点侧栏里那一行一样;不会自动发送,不替人换屏(只在人点了插件界面上的按钮时调,
873
852
  * 如「打开上次会话」打开插件经 `ctx.sessions.launch` 开出来的会话)。没有的会话抛错。
@@ -900,13 +879,6 @@ export interface UiContext {
900
879
  toolLine(tool: string, line: ToolLine, options?: {
901
880
  match?: "tool" | "prefix";
902
881
  }): void;
903
- /**
904
- * 给自己的工具画审批卡,见 `ApprovalCardProps`。`tool` 是 `ApprovalRequest.tool`;order 和让位规则同 `toolCard`:
905
- * 渲染出错时让给下一个,都不行用产品的通用卡。
906
- */
907
- approvalCard(tool: string, component: ComponentType<ApprovalCardProps>, options?: {
908
- order?: number;
909
- }): void;
910
882
  /**
911
883
  * 给 `run.request` 画卡,见 `RequestCardProps`。`kind` 是 `HumanRequest.kind`;order 和让位规则同 `toolCard`:渲染出错时让给下一个,
912
884
  * 都不行用产品的通用卡。
@@ -984,15 +956,18 @@ export interface UiContext {
984
956
  /** React hook:各插件登记的环境卡片分区,按 order 排好,给画环境卡片的插件用;增减跟着变。只在组件里调。 */
985
957
  useEnvironmentSections(): readonly EnvironmentSectionEntry[];
986
958
  /**
987
- * 输入框空着时的灰字建议(下一步想说什么),按 Tab 整段填进去,人看过再自己发。宿主画输入框时按注册先后问,
988
- * 取第一个给出字的;只问开着的会话,新对话不问。返回空串或 undefined 表示这次没有建议。
989
- * 只在渲染时同步调,别在里面做耗时的事。一个插件只能有一个,再注册换掉上一个。
959
+ * 输入框 `@` 菜单里加几项,见 `ComposerMention`:每次打开菜单时按输入框的目录和会话问一次,可以是 Promise;
960
+ * 菜单、分组、token 的样子由宿主画。一个插件一份,再登记换掉上一份,停用时撤下。
990
961
  */
991
- /** 每次打开 `@` 时列出当前目录可用的引用;停用后自动撤销,一个插件一份。 */
992
962
  mentions(provider: (context: {
993
963
  cwd?: string;
994
964
  sessionId?: string;
995
965
  }) => ComposerMention[] | Promise<ComposerMention[]>): void;
966
+ /**
967
+ * 输入框空着时的灰字建议(下一步想说什么),按 Tab 整段填进去,人看过再自己发。宿主画输入框时按注册先后问,
968
+ * 取第一个给出字的;只问开着的会话,新对话不问。返回空串或 undefined 表示这次没有建议。
969
+ * 只在渲染时同步调,别在里面做耗时的事。一个插件只能有一个,再注册换掉上一个。
970
+ */
996
971
  composerHint(hint: (session: SessionView) => string | undefined): void;
997
972
  /**
998
973
  * 换掉欢迎页问候下面的那一行(默认是「要在某个项目里开发什么?」):宿主每次画欢迎页时按界面语言问一次,等答复时那一行先空着,
@@ -1022,8 +997,13 @@ export interface UiContext {
1022
997
  /** 关闭自己在该会话打开的面板,仍经过该面板的 onClose;给了 `tab` 只关那一个标签。 */
1023
998
  closePanel(id: string, sessionId?: string, tab?: string): void;
1024
999
  /**
1025
- * 在指定会话的文件面板打开已有文件,读取仍受该会话的文件边界约束;唯一的例外是这个会话最近一次请求的「记忆文件」
1026
- * (`PromptSection.files` 标的项目指令、项目记忆来源),在边界外也能只读打开。
1000
+ * 把一段引用(`QuoteAction.run` 拿到的)放进自己面板 `panelId` 里 `ui.Conversation` 的输入框:会话 `sessionId` 眼前开着的正是这个面板时
1001
+ * 追加到它,不然新开一个标签放进去。草稿原样留着,不发送。面板没登记时抛错。输入框的草稿和引用归宿主,所以由宿主放。
1002
+ */
1003
+ quoteInPanel(panelId: string, sessionId: string, quote: DraftQuote): void;
1004
+ /**
1005
+ * 在指定会话的文件面板打开已有文件,读取仍受该会话的文件边界约束;会话媒体目录里的图(`ctx.media.save` 交回的 `path`)也算在边界里。
1006
+ * 唯一的例外是这个会话最近一次请求的「记忆文件」(`PromptSection.files` 标的项目指令、项目记忆来源),在边界外也能只读打开。
1027
1007
  */
1028
1008
  openFile(path: string, sessionId: string): Promise<void>;
1029
1009
  /**
@@ -1038,8 +1018,9 @@ export interface UiContext {
1038
1018
  droppedPaths(transfer: DataTransfer): string[] | null;
1039
1019
  /** 产品的文件右键菜单;root 用于复制相对路径,hook 只在组件里调用。 */
1040
1020
  useFileMenu(root: string): FileMenu;
1041
- /** 产品的语法高亮 diff 和文件预览;插件不需要另带一套代码渲染器。 */
1021
+ /** 产品的语法高亮 diff;插件不需要另带一套代码渲染器。 */
1042
1022
  readonly FileDiff: ComponentType<FileDiffProps>;
1023
+ /** 产品的文件预览(代码高亮、图片、文档),内容由插件经自己的 remote 读来,见 `FilePreviewProps`。 */
1043
1024
  readonly FilePreview: ComponentType<FilePreviewProps>;
1044
1025
  /** 按扩展名注册只读预览;同扩展名后注册的生效,撤下后恢复前一个。 */
1045
1026
  documentViewer(extensions: readonly string[], component: ComponentType<DocumentPreviewProps>): void;
@@ -1071,11 +1052,24 @@ export interface UiContext {
1071
1052
  * 一个插件只能有一个,再注册换掉上一个。
1072
1053
  */
1073
1054
  linkOpener(opener: LinkOpener): void;
1055
+ /**
1056
+ * 本体文件树等处「在终端中打开」由谁接:人点了以后调 `open(directory, sessionId)`,`directory` 是要进去的目录(绝对路径),
1057
+ * `sessionId` 是入口所在的会话。入口是本体的,终端是插件的,所以按种类登记成接手者。每个插件一个、再登记换掉上一个,
1058
+ * 几个插件都登记时最后登记的接,它停用后还给前一个;没人登记时本体提示去装终端插件,`open` 抛错时把错误弹成提示条。
1059
+ */
1060
+ terminalOpener(open: (directory: string, sessionId: string) => void | Promise<void>): void;
1061
+ /**
1062
+ * 回复里 shell 代码块的「在终端运行」:人点击后调用 `run(command, sessionId)`,`command` 已去掉提示符(`$ `、`> `)和注释行,
1063
+ * 要原样执行;`sessionId` 是代码块所在的会话。登记、让位和没有处理器时的提示同 `terminalOpener`。
1064
+ */
1065
+ terminalRunner(run: (command: string, sessionId: string) => void | Promise<void>): void;
1074
1066
  /**
1075
1067
  * 主输入框排队条上,每条排着的消息「更多」菜单里多一项,见 `QueueAction`;排在产品自己的「编辑」后面,几个插件按注册先后。
1076
1068
  * 没有插件登记时排队条直接放「编辑」按钮,不出菜单。一个插件只能有一个,再注册换掉上一个。
1077
1069
  */
1078
1070
  queueAction(action: QueueAction): void;
1071
+ /** 转录划词后引用菜单里多一项,见 `QuoteAction`。一个插件只能有一个,再注册换掉上一个,停用时撤下。 */
1072
+ quoteAction(action: QuoteAction): void;
1079
1073
  /** 用户点击时打开目标项目/目录的空白对话,已有草稿原样保留并追加 text;仍由人按发送。 */
1080
1074
  openConversationDraft(options: ConversationDraft): Promise<void>;
1081
1075
  /**
@@ -1165,6 +1159,11 @@ export interface UiContext {
1165
1159
  * 只在内存里,重开应用后都算看过。只在组件里调,规则同 React 的 hook。
1166
1160
  */
1167
1161
  useSessionActivity(sessionId: string): SessionActivity | undefined;
1162
+ /**
1163
+ * 订阅所有会话(包括没打开的)落盘的变化,见 `SessionChange`:只说哪个会话、哪一类变了,给跨会话的页面(画廊)知道该重读了;
1164
+ * 流式生成中的增量不发。返回取消订阅的函数,组件的 effect 收尾时调;插件停用时也撤下。会话事件只到渲染进程的宿主,所以由宿主转。
1165
+ */
1166
+ onSessionsChanged(listener: (event: SessionChange) => void): () => void;
1168
1167
  /**
1169
1168
  * 输入框 `/` 菜单此刻有的命令,同名去重后的样子:产品自己的(`/clear`,没有 `plugin`)在前,再是各插件经 `ctx.commands`
1170
1169
  * 供的。`cwd` 是按哪个目录列(项目里的命令文件、技能跟着变)。只读,展开、发送仍走输入框。每次调用现问,要看时再调。
@@ -1184,6 +1183,11 @@ export interface UiContext {
1184
1183
  readonly ModelSelect: ComponentType<ModelSelectProps>;
1185
1184
  /** 输入框里的模型和思考档位那两枚,见 `ModelControlsProps`。 */
1186
1185
  readonly ModelControls: ComponentType<ModelControlsProps>;
1186
+ /**
1187
+ * 主输入框的发送键,见 `ComposerSendProps`:发送和停止是同一枚,一轮开始、结束时在原地换。给插件自己画的输入框(画廊的改图条)用。
1188
+ * 跑着时按下停下 `sessionId` 这一轮,和主输入框的停止键一样:只有人按了这枚按钮才停,所以停止由按钮自己做,不开给插件直接调。
1189
+ */
1190
+ readonly ComposerSend: ComponentType<ComposerSendProps>;
1187
1191
  /** React hook:「跟随」会落到的模型和默认思考档位,见 `ModelDefaults`;设置改了跟着变。只在组件里调。 */
1188
1192
  useModelDefaults(): ModelDefaults;
1189
1193
  /** React hook:登记过的项目,按侧栏的顺序;增删改跟着变。只在组件里调。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeai/plugin-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "写 Plume 插件用的类型、命令(新建、检查、构建、安装)、插件模板和给 agent 的 skill。随 Plume 应用带一份,也发在 npm 上(插件仓库按版本依赖)。",
5
5
  "license": "MIT",
6
6
  "repository": {
package/skill/SKILL.md CHANGED
@@ -7,8 +7,8 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
7
7
 
8
8
  插件是一个目录,最多两个入口,都默认导出 `{ name, apply }`,`name` 相同:
9
9
 
10
- - **逻辑入口** `src/index.ts` → `dist/index.mjs`:跑在 agent 进程(Node),`apply(ctx)` 里注册工具、往钩子里加层、开接口给界面。
11
- - **界面入口** `src/ui.tsx` → `dist/ui.mjs`(可选):跑在渲染进程(浏览器,没有 Node),`apply(ui)` 里注册卡片、面板、页面、皮肤。
10
+ - **逻辑入口** `src/index.ts` → `dist/index.mjs`:跑在 agent 进程(Node),`apply(ctx)` 里注册工具、往钩子里加层、开接口给界面。细节读 [references/logic.md](references/logic.md)。
11
+ - **界面入口** `src/ui.tsx` → `dist/ui.mjs`(可选):跑在渲染进程(浏览器,没有 Node),`apply(ui)` 里注册卡片、面板、页面、皮肤。细节读 [references/ui.md](references/ui.md),组件查 [references/components.md](references/components.md),样式和文案读 [references/style-i18n.md](references/style-i18n.md)。
12
12
 
13
13
  两个入口一起启用、一起停用。所有注册都记在插件名下,停用或 `apply` 抛错时整体撤销,插件不用自己清理注册。
14
14
 
@@ -19,69 +19,79 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
19
19
  | 能用的 | 用来做什么 |
20
20
  | --- | --- |
21
21
  | 本 skill | 怎么写、有哪些约束 |
22
- | 插件 SDK `@plumeai/plugin-sdk` | 类型声明、`plume-plugin` 命令(新建、检查、构建、安装、开发)、tsconfig;下面这些模块名的类型都由它提供。随 Plume 应用发,不经 npm |
22
+ | 插件 SDK `@plumeai/plugin-sdk` | 类型声明、`plume-plugin` 命令(新建、检查、构建、安装、开发)、tsconfig;下面这些模块名的类型都由它提供。随 Plume 应用发,独立项目经 `plume-plugin init` 链到这一份,不用 npm 装 |
23
23
  | `@plume/contract`(逻辑入口)、`@plume/contract/ui`(界面入口) | 接口类型,以类型声明里的注释为准;只能 `import type` |
24
24
  | `react`、`react/jsx-runtime`、`@plume/ui`、`@plume/styles` | 界面入口运行时和产品共用同一份实例 |
25
25
  | `@plume/ui/icons` | 图标,打包进插件,只打用到的 |
26
26
  | `node:*`、自己 `package.json` 里声明的第三方包 | 逻辑入口随便用;界面入口只能用浏览器能跑的 |
27
27
  | 仓库里的插件(见文末) | 照着写 |
28
28
 
29
- 宿主运行时的一切从 `ctx` / `ui` 上拿。SDK 的类型里只有上表这些:`@plume/ui` 只开放 [references/components.md](references/components.md) 里的名字,引用别的类型检查就失败;`plume-plugin check` 还会查 import、色值和 Tailwind 类、写死的中文(Plume 的两个仓库里是 `pnpm check:plugins`,同一套规则)。
29
+ 宿主运行时的一切从 `ctx` / `ui` 上拿。`@plume/ui` 只开放 components.md 里的名字,引用别的类型检查就失败;`plume-plugin check` 还会查 import、色值和 Tailwind 类、写死的中文(Plume 的两个仓库里是 `pnpm check:plugins`,同一套规则)。
30
30
 
31
31
  ## 先判断能不能做成插件
32
32
 
33
- 插件能做的:
34
-
35
- - 给 agent 加工具(`ctx.tools.register`)。
36
- - 在每次工具调用外面加一层(钩子 `tool.execute`):预先放行、拦截、改参数、改结果、整个接管。
37
- - 一次操作要问人时先替人决定(钩子 `tool.approve`):放行、拒绝,或交给里层弹审批卡;完全访问、`approved`、「以后不再问」由本体放行、到不了这一层,提权只认人答(层只能拒绝);要人给答案(提问、计划、表单)不是审批,用 `run.request`,不经这一层,帮我批准的风险规则是内置插件 plume.approval 在这一层做的。
38
- - 在工具里跑 shell 命令(`run.exec`):沙箱档由本体按权限模式定,要更宽的一档经 `run.escalate` 申请(本体弹提权卡、只认人答、批了才换档),`run.shell()` 告诉你照哪个 shell 写;不自己起进程、不自己选档。
39
- - 给自己的工具画审批卡(`ui.approvalCard`):卡片摆在哪、排队、撤卡归产品,卡上画什么、有哪些按钮归插件。
40
- - 让一次工具调用停下来等人给一份数据(`run.request` + `ui.requestCard`):提问、批准计划、填表;不是权限,完全访问也照问,和审批同一队、同一格。
41
- - 每一轮开始、人的消息写进会话之前看一眼(钩子 `prompt.submit`):拦下这条消息(弹一条原因),或者放行并在它后面给模型补一段上下文。
42
- - 在每次调模型前改发出去的内容(钩子 `model.context`):按段加、换、删系统提示词,裁剪消息,隐藏工具。只影响这一次请求,会话里存的不变。
43
- - 每一步收尾时让循环多做一步、给结束换个原因(钩子 `step.end`):以用户身份对模型说一句再接着做,或者以出错、卡住结束这一轮;截断续写、空回复催促、重复提醒和久未汇报的提醒是本体在最里层做的。清单插件经 `StepEnd.progress` 提供汇报判定与提醒文案,本体统一计数;外层可以改写或换掉自然结论,拦不下本来要跑的那一步。
44
- - 人撤回或编辑重发时连文件一起退是本体做的:认的是本体记在工具结果上的改动(`ToolResultMessage.changes`)。工具经 `run.files.change` 写的、带 `recordChanges` 跑的命令改的,都跟着退;不用、也没有地方再插一层。
45
- - 给输入框的 `/` 菜单供命令(`ctx.commands.provide`):列出命令,发送时把 `/名字 参数` 展开成要发的话。
46
- - 工具卡片可读取 `argumentsText` / `streaming` 绘制安全的静态草稿,成功后经受用户操作校验的 `send(text, event)` 回到卡片所属会话;`ui.captureWindow(rect)` 只能截当前窗口可见区域。用法见 [references/ui.md](references/ui.md)。
47
- - 在对话里画某个工具的卡片(动态工具名可用 `ui.toolCard` 的 `match: "prefix"`),或者只登记它在转录里的说法(`ui.toolLine`:「查找符号 runAgent」、和读文件归成一类);在工作区加面板(标签按会话保留,按标签归属会话提供 `session` / `cwd`,子会话保留 owner 语义;不能用单例保存当前会话);在侧栏加页面;在输入框底下那排加一格(左边,或右边模型选择前面)、上方项目签那一行加一格;加一张跟着会话的卡片(浮在转录右上角或在输入框正上方);在设置页加一节;换皮肤(只能改 token,`mode` 支持 system/light/dark);经 `ui.ChatBackgroundSettings` 复用产品的背景设置,选图和拖入图片均由宿主校验。
48
- - 在最新结束的回复下面画卡片(`ui.turnFooter`),随转录滚走,新一轮开始就撤下;每一轮回复底下的操作排里加按钮(`ui.replyAction`);复用产品的高亮差异(`ui.FileDiff`)、文件预览(`ui.FilePreview`)、统一只读文档预览(`ui.DocumentPreview`;经 `ui.documentViewer` 按扩展名注册,读取与权限仍由调用方负责)、文件右键菜单(`ui.useFileMenu`),经 `ui.openFile` 打开指定会话工作目录内的文件。
49
- - 用 `ui.codeBlock(language, Component)` 接管助手正文的某种围栏代码块;解析、具体 UI、状态和使用说明都归插件,收到原文、会话/消息/块标识、流式与完成状态;可选 `copyText` 在复制回复时提供当前可见值的文字。解析失败返回 `fallback`;宿主在渲染失败或停用时回到源码。完整块里真实点击后可经 `send(text, event)` 回到所属会话,不能在渲染或计时器中发送。
50
- - 用 `ui.mentions` 给 `@` 菜单提供当前目录可用的智能体或技能引用;选中只插入 token,不自动发送。页面角标 `ui.pageBadge` 可带 `attention: true` 同时进入通知菜单。
51
- - 给输入框一句灰字建议,人按 Tab 填进去(`ui.composerHint`);换掉欢迎页问候下面的那一行(`ui.welcomeLine`);画回复前拿掉插件让模型顺带写的标记(`ui.replyText`,只改显示和复制);改会话标题(`ctx.sessions.setTitle`)。
52
- - 用设置里的生图模型画图、改图(`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()`)。
53
- - 侧栏会话行首空闲时画自己的记号(`ui.sessionMark`);删除确认框里补一句会连带、会留下什么(`ui.deleteNote`);人点了按钮时归档会话(`ctx.sessions.setArchived`);模型照人的要求归档它所在的对话时,工具那次调用经 `run.archiveAfterTurn()` 交给本体,这一轮以完成收场后才归档(只有顶层会话有)。
54
- - 弹提示条(界面入口 `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` 在压缩、撤回或上下文来源变化时作废自己的缓存。
55
- - 人点了明确的按钮时,经 `ui.openConversationDraft({ project?, cwd?, text })` 打开项目或目录的空白对话并准备草稿,保留已有草稿、不自动发送;经 `ui.openSettings(id)` 打开自己占导航一行(`page: "nav"`)的设置分区,经 `ui.openExternal(url)` 在系统浏览器打开不带凭据的 HTTP(S) 链接,经 `ui.openPath(path)` 把本机的文件用默认程序、文件夹用文件管理器打开,经 `ui.revealPath(path)` 在文件管理器里选中它(「在访达中显示」;逻辑入口只交回路径,不自己起 `open` / `explorer.exe` / `xdg-open`)。
56
- - 开一个新的顶层会话并发出第一句话(`ctx.sessions.launch`,定时任务到点了、人点了「在新会话里执行」):它进侧栏、不归插件,人接着聊,插件之后碰不到它;`done` 是第一轮怎么结束的,`idle` 在它第一次真闲下来时兑现(后台运行都回来、登记的一次性后台任务都结束、问出去的请求都有了答复、唤起的几轮也做完;挂着的监视、开发服务器、约好的唤醒不等),`reportBack(text, delivery)` 把结果交回开出它的会话一次(`idle` 兑现时带着最后一轮的收场和最后一段回复,交回用它);`spawnedFrom` 记着从哪个会话、哪次调用(`run.toolCallId`)开出来,要交回时那次调用得是你自己的工具在那个会话里的调用,不然 `reportBack` 抛错;人点了按钮时经 `ui.openSession(id)` 打开它。要把会话接到 Plume 之外(外面的人发消息、在外面答卡),开时带 `steward: true`:会话由你代管,之后经 `ctx.sessions.post` 往里转话、`ctx.sessions.respond` / `answer` 替人答卡,人随时能停止代管(见 references/logic.md「代管会话」)。页面那一行挂数目(`ui.pageBadge`),别处的按钮经 `ui.openPage(id)` 带人去自己的页面。
57
- - 建自己的子会话并发送消息(`ctx.sessions.create` / `send`),或经 `start` / `wait` 在后台跑、经 `report` 把该次结果送给真实父会话;`steer` 在运行中插话,`onStopped` 撤销尚未启动的排队工作。停止父会话后后台结果不能再唤醒它。自己的子会话里,自己的工具那次调用拿得到父会话(`run.parent`:`enqueue` 排进它的队列、`stop` 停下、`carryOn` 接着做、`status`),工具卡片上人点的按钮经 `handToParent` 把一件事排进父会话;作用对象只有这个父会话,写进去的话标着来自你的插件(显示名)、不算人的授权。`models` 只给无密钥的模型目录,建子会话时可选模型和思考档位。
58
- - 给自己的页面建一个一直在的助手:`ctx.sessions.create` 不给 `parentId` 就是插件的独立会话(不挂在任何会话下、不进侧栏,只有你能发),界面经 `ui.Conversation` 画它,审批卡就压在这个视图的输入框上方。
59
- - 界面经 `ui.Conversation`、`ui.useSession` 复用转录与实时状态;可局部定制任务消息、去掉回报重复正文,操控输入框用 `mode: "steer"`。`ui.Markdown` 复用正文和文件链接;`ui.composerAbove` 在主输入框上方独占一行。经 `ui.quoteAction` 接主会话划词菜单,`ui.quoteInPanel` 把引用追加到自己的面板草稿;`composer.draftKey` 按标签保存文字、附件和引用。`/` 命令可以自己办掉、不发消息(`{ handled: true }`)。
60
- - 界面经 `ui.onSessionsChanged` 订阅已保存消息、摘要和删除事件,刷新画廊等页面;经 `ui.ComposerSend` 复用发送/停止按钮,人真实点击停止时调用 `ui.stopSession`。终端插件经 `ui.terminalOpener` 承接文件树等入口的目录打开请求,经 `ui.terminalRunner` 承接代码块的「在终端运行」,经 `ui.findFile` / `ui.openFile` 打开输出里的路径、`ui.openLink` 打开网址、`ui.droppedPaths` 读拖进来的文件路径。
61
- - 截屏:经 `ui.screen` 开盖住屏幕的浮层、注册全局快捷键,浮层和钉在桌面上的小窗口里画自己的组件。
62
- - 在逻辑入口跑长期的东西(子进程、定时器、文件),经 `ctx.serve` / `ui.remote()` 给自己的界面用;HTTP(S) 请求可用 `ctx.fetch(url, init)` 继承系统代理,响应兼容标准 Fetch,禁止 URL 凭据、自动重定向和共享 cookie,插件停用自动取消。
63
- - 画网页:界面入口在面板里放 `<webview>`,逻辑入口经 `ctx.pages` 驱动它(加载、读、点按、输入、截图、缩放、视口、应答对话框);`ctx.pages.inspect(url, options)` 可在独立临时沙箱中检查页面,返回异常、控制台错误、高度和可选分段截图,不需要挂面板。面板设 `keepAlive`,标签在统一扁平常驻层,换会话、去设置页都不重挂,后台仍收到归属会话;`shown` 按是否正在显示所属会话的当前标签计算,`available` 按目录和有无会话分别计算、只控制新开列表,不卸载已有标签;设 `detachable`,弹出去的窗口里仍重新挂上。工具结果带图(`images`)给模型看;把字连同图片附件接进输入框(`ui.draftInSession`,不给会话就是眼前的输入框);回复里的网址、文件签多一种打开方式(`ui.linkOpener`);主输入框排队条上,排着的消息「更多」菜单里多一项、把这条拿到别处去发(`ui.queueAction`)。
64
- - 工具答完以后在后台接着跑的事,结束或有新进展时经 `run.background()` 的渠道送回这个会话(`report`,闲时开一轮、忙时在步骤边界读入,人按停后作废;只有顶层会话有)。还在跑的任务经 `run.track` 登记进本体(会自己结束的才算会话没闲,监视(`wakes`)和认出是长跑服务后经 `stand()` 标上的不算;删会话、退出时本体经你给的 `stop` 停掉),约会话过一会儿接着做经 `run.schedule`(到点经渠道送你给的话,不挡会话闲下来);别的插件经 `ctx.sessions.jobs` / `wakeups` 列出、停掉、取消。
65
- - 会话之间递话:顶层会话里,工具那次调用经 `run.letter(to, letter)` 给另一个顶层会话寄一封信(请求、答复或通知,本体照实填发件方、在前面说明不是用户的话),回执的 `read` 告诉你读到没有;请求的答复由本体配对,收件方没答、没读到、删掉了时本体代答,你不用记账。`run.letter`、`run.parent` 只在这次调用里能用,调用答完再用就抛错。
66
- - 工具里写、改用户的文件经 `run.files.change(path, edit)`:本体守写的边界(Plume 自己的数据目录、别的会话的临时目录不写,看着在工作区里的链接写不出去),同一个文件排队、写之前再核一遍没被别处改过,把这次改动记成快照、记在这次调用的结果上(`ToolOutput.changes`,本体填,工具写了也不算;交付卡、撤回据它退文件)。`edit` 拿到现在的 `{ path, text, stamp }`(不存在时 `text`、`stamp` 是 null),交回 `{ text?, value }`,不给 `text` 就不写;问不问人归工具自己(`run.ask`)。写资源地址(`scratch://`)经 `run.files.write(address, content)`;模型写的路径要换成绝对路径(显示、记「读过了」)用 `run.files.resolve(path)`。可能改文件的命令经 `run.exec(command, { recordChanges: true })` 跑,本体在命令前后对比工作区记下它改了什么;命令到这次调用答完还在跑的,读它输出的那次调用经 `run.files.includeChanges(process)` 把记下的算进来。读、撤销某个会话里记下的改动经 `ctx.changes.collect(sessionId, ids)`、`ctx.changes.undo(sessionId, ids, path?)`,`ids` 取工具结果的 `changes.ids`。
67
- - 工具里读用户的文件经 `run.files.read(path)`(路径或资源地址):本体守读的边界(工作区、项目目录、符号链接、密钥按文件问),和内置文件工具共用批准记录,只授予读取,成功后读交回的 `path`;跑命令的工具起命令前经 `run.files.readCommand(command)` 判命令写明要读的路径。列项目文件经 `ctx.workspace.files(root)`(git 认为该看的,不执行仓库配置的程序),不自己跑 `git ls-files`。
68
- - 把只为这次答案写的文件放进会话的临时目录(`run.scratchDir`,会话删掉时一起删):太大不值得交给模型的图写在这里、`output` 给路径、图放进结果的 `shownImages` 只给人看;经 `ctx.resources.register` 加一种资源地址(`scheme://路径`,模型用 read、ls 读它),或经 `ctx.resources.keepArtifact` 把大块原文存成 `artifact://` 地址。
69
-
70
- 插件做不到的:替换模型调用或 agent 循环、换掉别的工具(本体的、别的插件的)的审批卡、改模型回复(存下的那份;显示时拿掉标记见 `ui.replyText`)、在没有用户停止操作时让本来要接着做的一轮停下(`step.end` 只能多做或给结束换原因,`ui.stopSession` 只接受真实点击)、任意往会话里写东西或发起一轮(只允许自己的子会话和独立会话、钩子 `say` / `context`、宿主校验过的后台运行结果 `report`、工具经 `run.background()` 送回本会话的后台结果、工具经 `run.letter` 寄给另一个顶层会话的信(标着来自哪个会话)、`launch` 开出来的新会话的第一句话,以及自己的子会话里经 `run.parent` / `handToParent` 排进它父会话的任务;不能往别的已有会话里发、不能指定任意父会话或冒充用户授权)、没人点按钮时替人发送、绕过公开草稿接口操纵输入框、新增钩子或界面扩展点。
33
+ 逻辑入口能做的(成员和钩子的全部说明在 logic.md):
34
+
35
+ | 要做的事 | 用什么 |
36
+ | --- | --- |
37
+ | 给 agent 加工具 | `ctx.tools.register`;给模型的写进 `output`,给卡片的写进 `details`;带图给模型看用 `images` |
38
+ | 工具里跑 shell 命令 | `run.exec`:沙箱档由本体按权限模式定,要更宽一档经 `run.escalate` 申请(只认人答),照 `run.shell()` 的写法拼命令;不自己起进程 |
39
+ | 工具里读、写用户的文件 | `run.files`:本体守读写边界、记改动快照,人撤回时连文件一起退;可能改文件的命令带 `recordChanges` |
40
+ | 工具要问人放不放行,或要人给一份数据(提问、批准计划、填表) | `run.ask`;`run.request`(不是权限,完全访问也照问) |
41
+ | 工具答完后在后台接着做、送回结果,约会话过一会儿接着做,给另一个顶层会话寄信 | `run.background`、`run.track`、`run.schedule`、`run.letter`(除 `track` 外只有顶层会话有) |
42
+ | 在每次工具调用外面加一层:预先放行、拦截、改参数、改结果、整个接管 | 钩子 `tool.execute` |
43
+ | 一次操作要问人时先替人决定:放行、拒绝,或交给里层弹卡 | 钩子 `tool.approve`;完全访问、`approved`、「以后不再问」由本体放行、到不了这一层,提权只认人答 |
44
+ | 每一轮开始、人的消息写进会话前拦下它,或给模型补一段上下文 | 钩子 `prompt.submit` |
45
+ | 每次调模型前按段改系统提示词、裁剪消息、藏起工具(只影响这一次请求) | 钩子 `model.context` |
46
+ | 每一步收尾时让循环多做一步,或给结束换个原因 | 钩子 `step.end` |
47
+ | 换掉压缩的做法 | 钩子 `context.compact` |
48
+ | 给输入框的 `/` 菜单供命令 | `ctx.commands.provide` |
49
+ | 用默认模型做一次性的小调用、读花销、用生图模型出图 | `ctx.model`(`complete`、`usage`、`image`),图存进 `ctx.media` |
50
+ | 读会话、改标题、归档 | `ctx.sessions`(`list`、`messages`、`setTitle`、`setArchived`…) |
51
+ | 自己的子会话(侧边聊天、子 Agent),或页面上一直在的独立会话 | `ctx.sessions.create`,`send` / `start` / `wait` / `report` |
52
+ | 开一个新的顶层会话并发出第一句话;把会话接到 Plume 之外 | `ctx.sessions.launch`;带 `steward: true` 由你代管 |
53
+ | 列项目文件、在隔离视图里读 git、读写本体记下的改动 | `ctx.workspace`、`ctx.changes` |
54
+ | 加一种资源地址(`scheme://路径`) | `ctx.resources` |
55
+ | 跑长期的东西(子进程、定时器、文件)给自己的界面用 | `ctx.serve`(界面经 `ui.remote()` 调),清理登记 `ctx.effect` |
56
+ | 驱动面板里的网页(`<webview>`),或在临时沙箱里检查一页 | `ctx.pages` |
57
+ | HTTP(S) 请求走系统代理 | `ctx.fetch` |
58
+ | 插件之间提供、使用一组方法 | `ctx.provide` / `ctx.use` |
59
+
60
+ 界面入口能做的(成员的全部说明在 ui.md):
61
+
62
+ | 要做的事 | 用什么 |
63
+ | --- | --- |
64
+ | 工具在对话里的卡片,或只登记它在转录里的说法 | `ui.toolCard`、`ui.toolLine` |
65
+ | 给 `run.request` 画等人卡(审批卡只有产品那张) | `ui.requestCard` |
66
+ | 接管助手正文里某种语言的围栏代码块 | `ui.codeBlock` |
67
+ | 工作区加面板、侧栏加页面、设置页加一节 | `ui.panel`、`ui.page`、`ui.settingsSection` |
68
+ | 输入框周围:底下那排一格、上方项目签那一行一格、上方独占一行、灰字建议 | `ui.composerItem`、`ui.composerChip`、`ui.composerAbove`、`ui.composerHint` |
69
+ | 跟着会话的卡片、最新回复下面的卡片、回复操作排的按钮、环境卡片的一节 | `ui.conversationCard`、`ui.turnFooter`、`ui.replyAction`、`ui.environmentSection` |
70
+ | 画自己的子会话、独立会话 | `ui.Conversation`、`ui.useSession` |
71
+ | 复用产品的差异视图、文件预览、正文渲染、模型下拉 | `ui.FileDiff`、`ui.FilePreview`、`ui.Markdown`、`ui.ModelSelect` |
72
+ | 弹提示条、开关自己的面板、把字和图写进输入框 | `ui.notify`、`ui.openPanel`、`ui.draftInSession` |
73
+ | 换皮肤(只能改 token) | `ui.skin` |
74
+ | 截屏浮层、钉在桌面上的小窗口 | `ui.screen` |
75
+ | 侧栏记号、删除确认里的一句、网址和文件签的打开方式、划词菜单、排队消息的菜单…… | ui.md 的成员表 |
76
+
77
+ 以人的名义做的事(以人的身份发送、停下一轮、归档、打开会话)只在人点了按钮时做,不在渲染、定时器里做;停下一轮用 `ui.ComposerSend` 的停止键(交给它 `sessionId`),卡片的 `send` 由宿主核对是真实点击。
78
+
79
+ 插件做不到的:
80
+
81
+ - 替换模型调用或 agent 循环;换掉审批卡(本体只有一张);改存下的模型回复(显示时拿掉标记用 `ui.replyText`)。
82
+ - 在没有用户停止操作时让本来要接着做的一轮停下(`step.end` 只能多做或给结束换原因)。
83
+ - 任意往会话里写东西或发起一轮。能写的只有:自己的子会话和独立会话、钩子的 `say` / `context`、后台结果(`report`、`run.background()` 的渠道)、`run.letter` 寄的信(标着来自哪个会话)、`launch` 开出来的新会话的第一句、代管会话里转的话,以及自己的子会话里经 `run.parent` / `handToParent` 排进它父会话的任务。不能往别的已有会话里发、不能指定任意父会话或冒充用户授权。
84
+ - 没人点按钮时替人发送;绕过公开草稿接口操纵输入框;新增钩子或界面扩展点。
71
85
 
72
86
  **需求要用到做不到的能力时,停下来报告缺的是哪个能力、哪个插件需要它**,这是宿主的改动,不在插件里绕过(不改宿主代码、不碰宿主内部对象、不往页面上自己挂 DOM 冒充扩展点)。
73
87
 
74
88
  ## 流程
75
89
 
76
90
  1. **定名字**:插件名小写加点分段(Plume 自己提供的插件都以 `plume.` 开头);目录名只用小写字母、数字和连字符,它也是 `plume-plugin://` 的主机名。
77
- 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/<目录名>`(见下面的骨架),模板里是一个能跑的工具加卡片,改成自己的。
78
- 3. **逻辑入口**:读 [references/logic.md](references/logic.md)。
79
- 4. **界面入口**(要画东西时):读 [references/ui.md](references/ui.md)。
80
- 5. **样式和文案**(有界面或给人看的文字时):读 [references/style-i18n.md](references/style-i18n.md)。
81
- 6. **构建和加载**:
82
- - 独立项目:`plume-plugin dev` 构建、装进 `~/.plume/local-plugins/<目录名>/`(和市场装的同名时这一份生效,不被市场更新换掉,卸掉就回到市场那份),之后一改源码就重新构建、装上;Plume 开着的话每次都当场加载,不用重启。只装一次用 `plume-plugin build` 再 `plume-plugin install`。
83
- - Plume 的插件仓库里(`plugins/<目录名>`):`pnpm plugin dev plugins/<目录名>`,和独立项目一样构建、装进本机的 Plume。
84
- 7. **自检**:对照文末清单,再跑检查(独立项目 `plume-plugin check`:类型检查加插件规则;仓库里 `pnpm check`)。
91
+ 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/<目录名>`。
92
+ 3. **写逻辑入口**(logic.md),要画东西时**写界面入口**(ui.md、components.md),有界面或给人看的文字时照 style-i18n.md。
93
+ 4. **构建和加载**:独立项目用 `plume-plugin dev`,插件仓库里用 `pnpm plugin dev plugins/<目录名>`:构建、装进 `~/.plume/local-plugins/<目录名>/`(和市场装的同名时这一份生效,不被市场更新换掉,卸掉就回到市场那份),之后一改源码就重新构建、装上;Plume 开着的话每次都当场加载,不用重启。只装一次用 `plume-plugin build` 再 `plume-plugin install`。
94
+ 5. **自检**:对照下面的清单,再跑检查(独立项目 `plume-plugin check`:类型检查加插件规则;仓库里 `pnpm check`)。
85
95
 
86
96
  ## 目录骨架
87
97
 
@@ -92,7 +102,7 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
92
102
  ├── package.json # plume.displayName、plume.category 是「插件市场」里的名字和分类;仓库里还写 @plumeai/plugin-sdk 等开发依赖
93
103
  ├── tsconfig.json # 继承 @plumeai/plugin-sdk/tsconfig.json
94
104
  ├── README.md # 「插件市场」里的介绍:它做了什么、怎么用
95
- ├── images/ # 可选:README 引用的图片(相对路径 `images/xx.png`),随包分发
105
+ ├── images/ # 可选:README 引用的图片(相对路径 `images/xx.png`),随包分发
96
106
  ├── icon.png # 可选:插件图标,市场、详情和设置页共用,随安装包分发
97
107
  ├── AGENTS.md、CLAUDE.md、.claude/skills/plume-plugin/ # 只有独立项目有:给接手的 agent
98
108
  ├── node_modules/ # 只有独立项目有:init 建的链接,指到随 Plume 发的 SDK;坏了跑 plume-plugin link
@@ -104,15 +114,13 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
104
114
  └── types.ts # details 的类型和校验,两个入口共用
105
115
  ```
106
116
 
107
- 用 serve / remote 时再加 `src/api.ts`,放两个入口共用的接口类型。
117
+ 用 serve / remote 时再加 `src/api.ts`,放两个入口共用的接口类型;引入 `.css` 时加 `src/css.d.ts`(写法见 style-i18n.md)。
108
118
 
109
- - `package.json` 的 `description`、`keywords`、`plume.displayName`、`plume.category` 填好,「插件市场」里显示它们。
110
- - `plume.defaultPrompt` 可选,几句示例提问(字符串数组,最多显示四句):插件装上并开着时,详情页「试试这样问」列出它们,点一句开新对话、写进输入框。
119
+ - `package.json` 的 `description`、`keywords`、`plume.displayName`、`plume.category` 填好,「插件市场」里显示它们。`plume.defaultPrompt` 可选,几句示例提问(字符串数组,最多显示四句):插件装上并开着时,详情页「试试这样问」列出它们,点一句开新对话、写进输入框。
111
120
  - README 里的图片:插件目录里的写相对路径(放在 `images/` 下,支持 PNG、JPEG、GIF、WebP、SVG,每张不超过 5 MiB),网上的写 https 地址;带深色版本的 `<picture>` 按窗口明暗选。
112
- - 根目录的 `icon.png` 会自动作为插件图标,不用配置;使用正方形 PNG,建议 192×192,文件不超过 1 MiB。不提供或图片损坏时显示默认标记。
121
+ - 根目录的 `icon.png` 自动作为插件图标:正方形 PNG,建议 192×192,不超过 1 MiB;不提供或图片损坏时显示默认标记。
113
122
  - `devDependencies` 只用于类型和打包:逻辑入口用到的第三方包全部打进 `dist/index.mjs`。
114
123
  - `dependencies` 只放打不进去的原生模块(如 `node-pty`):构建时连同实际安装的运行依赖、可选依赖和 peer 依赖一起复制进 `dist/node_modules`,只保留 Plume 发包的平台、架构(darwin、win32 的 arm64、x64,加上构建机)的包与预编译文件,随 `dist/` 一起装。复制出的 Mach-O、ELF 文件(如 node-pty 的 `spawn-helper`)由构建补上执行位,不用为此写 postinstall。
115
- - 引入 `.css` 时加一个 `src/css.d.ts`(写法见 style-i18n.md)。
116
124
 
117
125
  ## 自检清单
118
126
 
@@ -121,33 +129,33 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
121
129
  - [ ] 工具结果里给模型的写进 `output`,给卡片的写进 `details`;卡片先校验 `details`,认不出就返回 `fallback`。
122
130
  - [ ] 钩子的层:不需要处理的调用原样 `next(input)`;`order` 选得对(见 logic.md);出错直接抛,不吞。
123
131
  - [ ] 逻辑入口自己起的定时器、子进程、监听登记了 `ctx.effect`;界面入口在 apply 中启动的订阅、远端 watch 登记 `ui.effect` 清理(如 `ui.effect(() => call.cancel())`),停用、重载或 apply 失败时都会执行。
124
- - [ ] 界面:没写 Tailwind 类;文字用 `Text`,组件只用 [references/components.md](references/components.md) 里列的;自己的 CSS 只管布局,颜色只取 `var(--plume-*)`;浅色、深色都能看。
125
- - [ ] 给人看的文字中英两份;发给模型的文字和日志标了 `i18n-exempt`。
132
+ - [ ] 界面:没写 Tailwind 类;文字用 `Text`,组件只用 components.md 里列的;自己的 CSS 只管布局,颜色只取 `var(--plume-*)`;浅色、深色都能看。
133
+ - [ ] 给人看的文字中英两份;代码里发给模型的中文标了 `i18n-exempt`。
126
134
  - [ ] `README.md` 写了它做什么、怎么用。
127
135
  - [ ] 类型检查通过(仓库里 `pnpm check` 通过);改了界面或行为的,装进 Plume 实际跑一遍。
128
136
 
129
137
  ## 可以照着写的插件
130
138
 
131
- 都在 Plume 插件仓库(plume-plugins)的 `plugins/` 下,`todo`、`commands`、`web`、`skills`、`git`、`side-chat`、`subagents`、`plan` 随应用发、在本体仓库的 `packages/builtin/` 下(拿不到仓库时,以模板和本 skill 里的代码片段为准):
139
+ `todo`、`commands`、`web`、`skills`、`git`、`mcp`、`browser`、`side-chat`、`subagents`、`plan` 随应用发,在本体仓库的 `packages/builtin/` 下;其余在 Plume 插件仓库(plume-plugins)的 `plugins/` 下。拿不到仓库时,以模板和本 skill 里的代码片段为准。
132
140
 
133
141
  | 插件 | 看它的什么 |
134
142
  | --- | --- |
135
- | `todo` | 注册工具、details 让本体的转录认得(不注册卡片,调用并进一段活);`step.end` 在本体的结论外面追问一句、工具结果带 `terminate` 结束这一轮、`run.calls` 看同一步的其他调用;跟着会话的卡片(`ui.conversationCard`)从会话的消息里读最新一份、按 `placement` 换外观、按钮经 `stop` / `carryOn` 停下或接着做;插件自带样式表;`messages.ts` 的写法 |
136
- | `terminal` | `ctx.serve` / `ui.remote()` 流式输出和取消、原生依赖、`ctx.effect`、多标签面板、插件自带样式表、点链接打开文件和网址、拖文件进来粘贴路径 |
137
- | `scheduled` | `ctx.dataDir` 存数据、页面(`ui.page`)经 `ui.remote()` 读写;逻辑入口常驻定时器按时 `ctx.sessions.launch` 开新会话、等第一轮结束判失败;`watch` 推状态,界面入口一直连着、给页面挂数目(`ui.pageBadge`)、在输入框上方说失败(`ui.composerAbove`、`ui.openPage`),卡片上「打开上次会话」(`ui.openSession`) |
138
- | `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`、设置分区 |
139
- | `memory` | `model.context` 按段插提示词、每个会话冻结一份;按条件对模型藏起工具;`ctx.sessions` 读旧会话、`ctx.model.complete` 在后台定时总结;设置分区画进「个性化」页(`page`),「智能体」页「会话」那张卡里一行选总结用的模型(`ui.ModelSelect`) |
140
- | `usage` | 设置 › 数据里的一页(`ui.settingsSection` 的 `page: "nav"` 加 `group: "data"`,页头介绍用 `detail`);逻辑入口经 `ctx.model.usage` 读花销、`ctx.serve` 转给界面;SVG 自己画的堆叠柱状图(悬停气泡 fixed 定位)、环图(`stroke-dasharray` 分段)、热力图和色点,插件自带的模型品牌标 |
141
- | `screenshot` | 截屏浮层(`ui.screen`、`ui.screenOverlay`、`ui.pinnedShot`)、标注组件、输入框按钮(`ui.composerItem`)、占导航一行的设置分区(`ui.settingsSection` 的 `page: "nav"`)、逻辑入口跑子进程 |
142
- | `commands` | `ctx.commands.provide` 供 `/` 命令(每次现读磁盘);设置分区经 `ui.remote()` 新建、打开、删除文件 |
143
- | `hooks` | 四个钩子一起用:`prompt.submit` 拦消息、补上下文,`tool.execute` 预先放行、要确认(`input.ask`)、改参数、接结果,`tool.approve`(order 2000)替人答审批,`step.end` 让它接着干;`ctx.sessions.observe` 的 `turnEnd`(默认导出写 `observe: "all"`,看每个会话)和 `ctx.sessions.onEnded` 接一轮失败、会话结束;逻辑入口跑子进程,项目钩子读 `.plume/config.json` 的 `hooks`、按内容指纹信任;运行记录存在 `ctx.dataDir`、`watch` 推变化,`ui.replyAction` 在回复底下看、`ui.composerAbove` 提示待审核、`ui.openSettings` 去设置分区;设置分区里 `ui.useProjects` 切范围、表单弹窗 |
144
- | `im` | 不给 `page` 的设置分区:画进插件详情的「设置」一节,不占设置导航;密钥经 `ui.remote()` 交给逻辑入口存进 `ctx.dataDir`,界面只知道存没存 |
145
- | `index` | `ctx.workspace.files` 列项目文件;按会话缓存工具要用的数据、会话删掉时丢掉;设置分区用 `ui.useProjects` 列项目、经 `ui.remote()` 看状态、触发重建 |
146
- | `web` | 只读的工具(不弹审批卡)、`ctx.dataDir` 存 API key、key 不经界面(界面只知道填没填) |
147
- | `mcp` | 运行中途增减工具:`ctx.tools.register` 返回的函数撤下单个工具;逻辑入口连外部服务、调用前 `run.ask`;大图存进 `ctx.media`、放进结果的 `shownImages` 由通用卡片画;服务要人填表时 `run.request` 带一句 `waiting`、卡片经 `ui.requestCard` 按 `kind` 登记;`model.context` 按估算的大小藏起工具、换成搜索入口;`ctx.sessions.onDeleted` 关按会话起的连接;`ctx.dataDir` 存配置和令牌,密钥不经界面;设置分区里的卡片(就地编辑、失焦保存)、弹窗和 `Popover` 菜单 |
148
- | `skills` | `model.context` 按磁盘现状加一段、没东西时藏起自己的工具;`tool.execute` 的一层按会话状态拒掉调用;`prompt.submit` 的 `context` 给模型补一句;`ctx.commands.provide` 把技能放进 `/` 菜单、`displayText` 只给人看;设置分区里用 `ui.FileDiff` 画差异 |
149
- | `subagents` | `start` / `wait` / `report` 后台派发和回报,按原子会话续跑,`steer` 插话;一个工具名、参数按会话换(`model.context` 改 `yield` 的参数为定义的输出结构),结果带 `terminate` 收尾,`step.end`(order -20)在父会话里有子代理没回来时经 `awaiting` 告诉里层别逼它交结论、每 4 次带工具的请求提醒一次在等谁,在子会话里每 60 步一个检查点,读子会话里最新的 `todo_write` 结果决定接着跑、`say` 一句让它列清单或讨交接(`stepsLeft` 只剩两步时也讨),讨交接的两步 `tool.execute` 拒掉别的工具、第二步的结果带 `terminate`;插件管理种类、名额和排队,停止监听清队列(子会话那一段跑完时由本体停掉它起的后台任务);`composerAbove`、任务卡和回报卡,`ui.Conversation` 局部渲染与附件操控;「智能体」整页(`page: "agents"`):读写定义文件、`ui.useProjects` 切范围、`ctx.tools.names` 挑工具、`ui.ModelSelect` 选模型、插件自己的选脸弹层 |
150
- | `plan` | 按会话记的开关(`ui.composerItem` 里的 `ComposerToggle`、`/plan`,新对话的开关经 `prompt.submit` 交给建出来的会话);`tool.execute` 每次调用设 `readOnly`(带拒绝时给模型的话,写、改文件的工具由本体拒绝);工具经 `run.request` 带 `waiting`(「等你批准计划」)等人拍板,自己画卡(`ui.requestCard`),附言随回答一起交回;面板里划线评论(CSS Custom Highlight);`context.compact` 往摘要后面接上批准的计划;`model.context` 加一段、`step.end` 提醒一次;`submit_plan` 经 `ui.toolLine` 在转录里读成一行,环境卡片里一节(`ui.environmentSection`)列出批准过的计划、点了打开面板,面板从会话消息里读 |
151
- | `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/` |
152
- | `side-chat` | 面板里的多段子会话,`ui.Conversation` 带输入框;`prompt.submit` 给每一问附上主会话的增量,主会话的下一问附上侧边聊天改过的文件(经 `ctx.changes` 认本体记在工具结果上的改动);`/btw` 自己办掉(`handled`、`openPanel`);排队消息的「在侧边聊天中打开」(`ui.queueAction`);关标签前用 `Confirm` 问一句;「会话」卡里一行选新侧边聊天的模型(`page: "session-models"`) |
153
- | `image` | 用设置里的生图模型出图(`ctx.model.image`),图存进媒体目录(`ctx.media`);工具卡片经进度推 JSON 行、边画边显示,`ui.mediaUrl` 显示、`ui.openImage` 看图和标注、`ui.draftInSession` 把图带回会话;`model.context` 没有生图模型时藏起工具;画廊页(`ui.page`)经 `ui.remote()` 扫全部会话、`ui.revealToolCall` 跳回那次调用 |
143
+ | `todo` | 工具的 details 让本体的转录认得(`category: "checklist"`,不注册卡片);`step.end` 在本体的结论外面追问一句;工具结果带 `terminate`;`run.calls`;跟着会话的卡片(`ui.conversationCard`)按 `placement` 换外观、`stop` / `carryOn`;插件自带样式表;`messages.ts` |
144
+ | `terminal` | `ctx.serve` / `ui.remote()` 流式输出和取消;原生依赖;`ctx.effect`;多标签面板;点链接打开文件和网址;拖文件进来粘贴路径 |
145
+ | `scheduled` | `ctx.dataDir` 存数据;页面(`ui.page`);常驻定时器按时 `ctx.sessions.launch`;`watch` 推状态、`ui.pageBadge` 挂数目、`ui.composerAbove` 说失败、`ui.openPage`、`ui.openSession` |
146
+ | `git` | 大一点的插件:子进程、`ctx.workspace.git`、`ctx.model.complete`;面板里多个视图、`.cancel()` 叫停远端调用;`ui.composerChip` 的 `send`;`ctx.provide` 提供服务 `session.worktree`;对话自己的工作树(`prompt.submit` 的 `cwd`、`ctx.sessions.onDeleted` / `onArchived`、`ui.sessionMark`、`ui.deleteNote`) |
147
+ | `memory` | `model.context` 按段插提示词、每个会话冻结一份;`ctx.model.complete` 在后台总结;设置分区画进「个性化」页、「智能体」页「会话」卡里一行(`ui.ModelSelect`) |
148
+ | `usage` | 设置 › 数据里的一页(`page: "nav"`、`group: "data"`);`ctx.model.usage`;SVG 自己画的图表 |
149
+ | `screenshot` | `ui.screen`、`ui.screenOverlay`、`ui.pinnedShot`;标注组件;`ui.composerItem`;占导航一行的设置分区 |
150
+ | `commands` | `ctx.commands.provide`(每次现读磁盘);设置分区经 `ui.remote()` 新建、打开、删除文件 |
151
+ | `hooks` | 六个钩子里的四个一起用(`tool.approve` 在 order 2000);`observe: "all"` 和 `ctx.sessions.onEnded`;运行记录 `watch` 推变化、`ui.replyAction`、`ui.composerAbove`、`ui.openSettings`;`ui.useProjects` 切范围 |
152
+ | `im` | 不给 `page` 的设置分区(画进插件详情);密钥经 `ctx.secrets` 存进 `ctx.dataDir`,界面只知道存没存 |
153
+ | `index` | `ctx.workspace.files`;按会话缓存、会话删掉时丢掉;设置分区里 `ui.useProjects` |
154
+ | `web` | 只读的工具(不弹审批卡);API key 不经界面 |
155
+ | `mcp` | 运行中途增减工具;`run.ask`;`run.request` 带 `waiting` 和 `ui.requestCard`;大图放进 `shownImages`;`model.context` 藏工具换成搜索入口;前缀登记卡片;设置分区里的就地编辑、弹窗和 `Popover` 菜单 |
156
+ | `skills` | `model.context` 按磁盘现状加一段;`tool.execute` 按会话状态拒掉调用;`/` 菜单的 `displayText`;`ui.FileDiff` |
157
+ | `subagents` | `start` / `wait` / `report` 后台派发和回报,`steer` 插话;`model.context` 按会话换参数;`step.end` 的 `awaiting`;`ui.Conversation` 局部渲染;「智能体」整页(`page: "agents"`)、`ctx.tools.list()` 挑工具、`ui.ModelSelect` |
158
+ | `plan` | `ui.composerItem` 里的 `ComposerToggle`;`tool.execute` 设 `readOnly`;`run.request` 带 `waiting`、自己画卡;`context.compact` 接上批准的计划;`ui.toolLine`;`ui.environmentSection` |
159
+ | `browser` | `<webview>` + `ctx.pages`;面板 `keepAlive` + `detachable`;`ui.openPanel` 的 `tab` / `background`、`ui.closePanel`;工具结果带图;`ui.linkOpener`;随包的 `skills/` |
160
+ | `side-chat` | 面板里的多段子会话(`ui.Conversation` 带输入框);`prompt.submit` 补上下文;`ctx.changes`;`/btw` 自己办掉(`handled`);`ui.queueAction`;关标签前用 `Confirm` 问一句 |
161
+ | `image` | `ctx.model.image`、`ctx.media`;工具卡片边画边显示;`ui.mediaUrl`、`ui.openImage`、`ui.draftInSession`;画廊页经 `ui.remote()` 扫全部会话、`ui.revealToolCall` |