@plumeai/plugin-sdk 0.1.7 → 0.2.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.
package/lib/build.mjs CHANGED
@@ -6,13 +6,16 @@
6
6
  * 打不进去的(node-pty 这类原生模块)写在那里,不打包,原样复制进 dist/node_modules,运行时按 Node 的解析找到。
7
7
  * 一并复制实际安装的运行依赖闭包;按平台分的预编译(`os`/`cpu` 分包、prebuilds/<平台>-<架构>)带上 targets.mjs 里
8
8
  * 那几个平台、架构装了的每一份:dist/ 会进 Plume 的多架构安装包,打包时再按目标裁(见 targets.mjs)。
9
+ * 复制出的 Mach-O、ELF 文件补上执行位,不靠依赖的安装脚本(见 markExecutable)。
9
10
  * - 界面入口:浏览器 ESM;react、react/jsx-runtime、@plume/ui、@plume/styles 留作外部,运行时经宿主的 import map 用产品的实例。
10
11
  * 只认这几个模块名本身:@plume/ui/icons 不在 import map 里,指到 SDK 自带的图标库打进插件(只打用到的那几个图标)。
11
12
  * 引入的 .css 是一段文字,插件自己挂到页面上(宿主不加载插件的样式表)。
13
+ * 宿主启动时就加载每个插件的 dist/ui.mjs;`import()` 进来的模块拆到 dist/chunks/,用到时才取,大的预览库不拖慢启动。
14
+ * 拆出来的文件名带内容哈希,和 ui.mjs 一起经 `plume-plugin://` 读取。
12
15
  */
13
16
 
14
17
  import { existsSync } from "node:fs";
15
- import { cp, readFile, realpath, rm } from "node:fs/promises";
18
+ import { cp, open, readdir, readFile, realpath, rm } from "node:fs/promises";
16
19
  import { createRequire } from "node:module";
17
20
  import { basename, dirname, join, resolve, sep } from "node:path";
18
21
  import { build } from "esbuild";
@@ -32,6 +35,29 @@ const shared = {
32
35
  };
33
36
  const HOST = `${process.platform}-${process.arch}`;
34
37
 
38
+ /** Mach-O(单架构 32、64 位的两种字节序,通用二进制)和 ELF 的文件头。 */
39
+ const NATIVE_MAGIC = new Set(["feedface", "feedfacf", "cefaedfe", "cffaedfe", "cafebabe", "7f454c46"]);
40
+
41
+ /**
42
+ * 给 dir 下的 Mach-O、ELF 文件补上执行位。复制保留依赖安装时的权限,有的预编译包发布时就没带执行位、靠安装脚本补
43
+ * (node-pty 1.1.0 的 spawn-helper);依赖按 `--ignore-scripts` 装时它是 0644,`plume-plugin dev` / `install` 原样装进去,
44
+ * 终端起 shell 报 `posix_spawnp failed.`。按文件头认、不按包名认;动态库(`.node`、`.dylib`、`.so`)一起加上无害。
45
+ * 只改权限、不改内容。Windows 没有执行位,跳过。
46
+ */
47
+ async function markExecutable(dir) {
48
+ if (process.platform === "win32") return;
49
+ for (const entry of await readdir(dir, { recursive: true, withFileTypes: true })) {
50
+ if (!entry.isFile()) continue;
51
+ const file = await open(join(entry.parentPath, entry.name), "r");
52
+ try {
53
+ const { bytesRead, buffer } = await file.read(Buffer.alloc(4), 0, 4, 0);
54
+ if (bytesRead === 4 && NATIVE_MAGIC.has(buffer.toString("hex"))) await file.chmod((await file.stat()).mode | 0o111);
55
+ } finally {
56
+ await file.close();
57
+ }
58
+ }
59
+ }
60
+
35
61
  /** 复制实际安装的运行依赖,顶层同名冲突时放到依赖它的包下面。 */
36
62
  async function copyDependencies(dir, names) {
37
63
  const target = join(dir, "dist/node_modules");
@@ -103,6 +129,7 @@ async function copyDependencies(dir, names) {
103
129
  roots.push({ pkg, destination });
104
130
  }
105
131
  for (const { pkg, destination } of roots) await copyPackage(pkg, destination);
132
+ if (roots.length > 0) await markExecutable(target);
106
133
  }
107
134
 
108
135
  /**
@@ -125,11 +152,16 @@ export async function buildPlugin(target) {
125
152
  await build({ ...common, entryPoints: [logic], outfile: join(dir, "dist/index.mjs"), platform: "node", target: "node24", external: native, banner });
126
153
  await copyDependencies(dir, native);
127
154
  const outputs = [`dist/index.mjs${native.length > 0 ? `(带上 ${native.join("、")})` : ""}`];
155
+ await rm(join(dir, "dist/chunks"), { recursive: true, force: true });
128
156
  if (existsSync(ui)) {
129
157
  await build({
130
158
  ...common,
131
159
  entryPoints: [ui],
132
- outfile: join(dir, "dist/ui.mjs"),
160
+ outdir: join(dir, "dist"),
161
+ splitting: true,
162
+ entryNames: "[name]",
163
+ chunkNames: "chunks/[name]-[hash]",
164
+ outExtension: { ".js": ".mjs" },
133
165
  platform: "browser",
134
166
  target: "chrome156",
135
167
  jsx: "automatic",
package/lib/lint/i18n.mjs CHANGED
@@ -18,6 +18,7 @@ const CHINESE = /[一-鿿、,:;()「」『』。!?]/;
18
18
  /**
19
19
  * 去掉注释,换行保留(块注释里的也一个换一个),行号才对得上,`i18n-exempt` 也按行认。
20
20
  * 认得字符串:示例 URL 里的 `//` 不是注释,当成注释会把那一行后面的文字都跳过去。
21
+ * 认得正则字面量:`/"/g` 里的引号不是字符串的开头,当成字符串会把后面的注释吞进去、报成文案。
21
22
  * 模板字符串会嵌套,`${…}` 里又可以有字符串和注释,所以用一个栈。
22
23
  */
23
24
  function stripComments(src) {
@@ -62,6 +63,15 @@ function stripComments(src) {
62
63
  out += body.replace(/[^\n]/g, "");
63
64
  continue;
64
65
  }
66
+ if (char === "/" && regexMayStart(out)) {
67
+ const end = regexEnd(src, i);
68
+ if (end !== -1) {
69
+ // 正则不是给人读的字,内容遮成空格,后面认字符串时也不会再被里面的引号带偏。
70
+ out += `/${src.slice(i + 1, end).replace(/[^\n]/g, " ")}/`;
71
+ i = end + 1;
72
+ continue;
73
+ }
74
+ }
65
75
  if (char === "'" || char === '"' || char === "`") stack.push(char);
66
76
  else if (char === "{") stack.push("{");
67
77
  else if (char === "}" && state === "{") stack.pop();
@@ -71,6 +81,36 @@ function stripComments(src) {
71
81
  return out;
72
82
  }
73
83
 
84
+ /** 斜杠前面是这些词时,它开始的是正则,不是除号(`return /x/.test(s)`)。 */
85
+ const BEFORE_REGEX_WORDS = new Set(["return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw", "case", "do", "else", "yield", "await"]);
86
+
87
+ /**
88
+ * 代码里的 `/` 是不是正则的开头:看前一个有意义的字符。跟在值后面(标识符、数字、`)`、`]`、`}`)是除号,跟在运算符、
89
+ * 括号、逗号后面是正则。`<` 不算(`</p>` 是 JSX 的结束标签),`>` 只认箭头函数的 `=>`(`<p>/路径</p>` 里的是 JSX 文本)。
90
+ */
91
+ function regexMayStart(out) {
92
+ const code = out.trimEnd();
93
+ const last = code.at(-1);
94
+ if (last === undefined) return true;
95
+ if (last === ">") return code.at(-2) === "=";
96
+ if (/[\w$]/.test(last)) return BEFORE_REGEX_WORDS.has(/[\w$]+$/.exec(code)[0]);
97
+ return "(,=:[!&|?{};+-*%~^".includes(last);
98
+ }
99
+
100
+ /** 从 `start` 的 `/` 起,正则收尾那个 `/` 的位置:认得转义和字符类 `[/]`;同一行里收不了尾就不是正则,返回 -1。 */
101
+ function regexEnd(src, start) {
102
+ let inClass = false;
103
+ for (let i = start + 1; i < src.length; i++) {
104
+ const char = src[i];
105
+ if (char === "\n") return -1;
106
+ if (char === "\\") i += 1;
107
+ else if (char === "[") inClass = true;
108
+ else if (char === "]") inClass = false;
109
+ else if (char === "/" && !inClass) return i;
110
+ }
111
+ return -1;
112
+ }
113
+
74
114
  /** 只进日志的调用:`console.*`,和 Plume 自己的日志函数(`log(…)`、`this.options.log(…)`、`ctx.log.warn(…)`)。 */
75
115
  const LOG_CALL = /\b(?:console\s*\.\s*(?:log|info|warn|error|debug|trace)|(?:\w+\s*\.\s*)*log(?:\s*\.\s*(?:info|warn|error))?)\s*\(/g;
76
116
 
@@ -141,9 +181,7 @@ export function chineseFindings(source) {
141
181
  }
142
182
  const masked = stripped.replace(literal, (text) => text.replace(/[^\n]/g, " "));
143
183
  masked.split("\n").forEach((line, index) => {
144
- // 字符串已经遮住,剩下的 `//` 是注释(正则字面量里的引号可能让前面没认出它)。
145
- const code = line.replace(/\/\/.*$/, "");
146
- if (CHINESE.test(code)) keep(index + 1, code.trim().replace(/\s+/g, " ").slice(0, 80));
184
+ if (CHINESE.test(line)) keep(index + 1, line.trim().replace(/\s+/g, " ").slice(0, 80));
147
185
  });
148
186
  return found;
149
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plumeai/plugin-sdk",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
4
4
  "description": "写 Plume 插件用的类型、命令(新建、检查、构建、安装)、插件模板和给 agent 的 skill。随 Plume 应用带一份,也发在 npm 上(插件仓库按版本依赖)。",
5
5
  "license": "MIT",
6
6
  "repository": {
package/skill/SKILL.md CHANGED
@@ -34,12 +34,14 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
34
34
 
35
35
  - 给 agent 加工具(`ctx.tools.register`)。
36
36
  - 在每次工具调用外面加一层(钩子 `tool.execute`):预先放行、拦截、改参数、改结果、整个接管。
37
- - 一次操作要问人时先替人决定(钩子 `tool.approve`):放行、拒绝,或交给里层弹审批卡;完全访问、`approved`、「以后不再问」由本体放行、到不了这一层,提权和 `userOnly` 只认人答(层只能拒绝),帮我批准的风险规则是内置插件 plume.approval 在这一层做的。
37
+ - 一次操作要问人时先替人决定(钩子 `tool.approve`):放行、拒绝,或交给里层弹审批卡;完全访问、`approved`、「以后不再问」由本体放行、到不了这一层,提权只认人答(层只能拒绝);要人给答案(提问、计划、表单)不是审批,用 `run.request`,不经这一层,帮我批准的风险规则是内置插件 plume.approval 在这一层做的。
38
+ - 在工具里跑 shell 命令(`run.exec`):沙箱档由本体按权限模式定,要更宽的一档经 `run.escalate` 申请(本体弹提权卡、只认人答、批了才换档),`run.shell()` 告诉你照哪个 shell 写;不自己起进程、不自己选档。
38
39
  - 给自己的工具画审批卡(`ui.approvalCard`):卡片摆在哪、排队、撤卡归产品,卡上画什么、有哪些按钮归插件。
40
+ - 让一次工具调用停下来等人给一份数据(`run.request` + `ui.requestCard`):提问、批准计划、填表;不是权限,完全访问也照问,和审批同一队、同一格。
39
41
  - 每一轮开始、人的消息写进会话之前看一眼(钩子 `prompt.submit`):拦下这条消息(弹一条原因),或者放行并在它后面给模型补一段上下文。
40
42
  - 在每次调模型前改发出去的内容(钩子 `model.context`):按段加、换、删系统提示词,裁剪消息,隐藏工具。只影响这一次请求,会话里存的不变。
41
43
  - 每一步收尾时让循环多做一步、给结束换个原因(钩子 `step.end`):以用户身份对模型说一句再接着做,或者以出错、卡住结束这一轮;截断续写、空回复催促、重复提醒和久未汇报的提醒是本体在最里层做的。清单插件经 `StepEnd.progress` 提供汇报判定与提醒文案,本体统一计数;外层可以改写或换掉自然结论,拦不下本来要跑的那一步。
42
- - 人撤回或编辑重发、要截掉一段对话之前,答那段里自己记着的改动能不能退回,人选了就退回去(钩子 `session.rewind`);截断是本体做的,文件的退回是内置插件 plume.delivery 在这一层做的。
44
+ - 人撤回或编辑重发时连文件一起退是本体做的:认的是本体记在工具结果上的改动(`ToolResultMessage.changes`)。工具经 `run.files.change` 写的、带 `recordChanges` 跑的命令改的,都跟着退;不用、也没有地方再插一层。
43
45
  - 给输入框的 `/` 菜单供命令(`ctx.commands.provide`):列出命令,发送时把 `/名字 参数` 展开成要发的话。
44
46
  - 工具卡片可读取 `argumentsText` / `streaming` 绘制安全的静态草稿,成功后经受用户操作校验的 `send(text, event)` 回到卡片所属会话;`ui.captureWindow(rect)` 只能截当前窗口可见区域。用法见 [references/ui.md](references/ui.md)。
45
47
  - 在对话里画某个工具的卡片(动态工具名可用 `ui.toolCard` 的 `match: "prefix"`),或者只登记它在转录里的说法(`ui.toolLine`:「查找符号 runAgent」、和读文件归成一类);在工作区加面板(标签按会话保留,按标签归属会话提供 `session` / `cwd`,子会话保留 owner 语义;不能用单例保存当前会话);在侧栏加页面;在输入框底下那排加一格(左边,或右边模型选择前面)、上方项目签那一行加一格;加一张跟着会话的卡片(浮在转录右上角或在输入框正上方);在设置页加一节;换皮肤(只能改 token,`mode` 支持 system/light/dark);经 `ui.ChatBackgroundSettings` 复用产品的背景设置,选图和拖入图片均由宿主校验。
@@ -50,19 +52,20 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
50
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()`)。
51
53
  - 侧栏会话行首空闲时画自己的记号(`ui.sessionMark`);删除确认框里补一句会连带、会留下什么(`ui.deleteNote`);人点了按钮时归档会话(`ctx.sessions.setArchived`);模型照人的要求归档它所在的对话时,工具那次调用经 `run.archiveAfterTurn()` 交给本体,这一轮以完成收场后才归档(只有顶层会话有)。
52
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` 在压缩、撤回或上下文来源变化时作废自己的缓存。
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` 只给无密钥的模型目录,建子会话时可选模型和思考档位。
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` 只给无密钥的模型目录,建子会话时可选模型和思考档位。
56
58
  - 给自己的页面建一个一直在的助手:`ctx.sessions.create` 不给 `parentId` 就是插件的独立会话(不挂在任何会话下、不进侧栏,只有你能发),界面经 `ui.Conversation` 画它,审批卡就压在这个视图的输入框上方。
57
59
  - 界面经 `ui.Conversation`、`ui.useSession` 复用转录与实时状态;可局部定制任务消息、去掉回报重复正文,操控输入框用 `mode: "steer"`。`ui.Markdown` 复用正文和文件链接;`ui.composerAbove` 在主输入框上方独占一行。经 `ui.quoteAction` 接主会话划词菜单,`ui.quoteInPanel` 把引用追加到自己的面板草稿;`composer.draftKey` 按标签保存文字、附件和引用。`/` 命令可以自己办掉、不发消息(`{ handled: true }`)。
58
60
  - 界面经 `ui.onSessionsChanged` 订阅已保存消息、摘要和删除事件,刷新画廊等页面;经 `ui.ComposerSend` 复用发送/停止按钮,人真实点击停止时调用 `ui.stopSession`。终端插件经 `ui.terminalOpener` 承接文件树等入口的目录打开请求,经 `ui.terminalRunner` 承接代码块的「在终端运行」,经 `ui.findFile` / `ui.openFile` 打开输出里的路径、`ui.openLink` 打开网址、`ui.droppedPaths` 读拖进来的文件路径。
59
61
  - 截屏:经 `ui.screen` 开盖住屏幕的浮层、注册全局快捷键,浮层和钉在桌面上的小窗口里画自己的组件。
60
62
  - 在逻辑入口跑长期的东西(子进程、定时器、文件),经 `ctx.serve` / `ui.remote()` 给自己的界面用;HTTP(S) 请求可用 `ctx.fetch(url, init)` 继承系统代理,响应兼容标准 Fetch,禁止 URL 凭据、自动重定向和共享 cookie,插件停用自动取消。
61
63
  - 画网页:界面入口在面板里放 `<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`)。
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://` 地址。
66
69
 
67
70
  插件做不到的:替换模型调用或 agent 循环、换掉别的工具(本体的、别的插件的)的审批卡、改模型回复(存下的那份;显示时拿掉标记见 `ui.replyText`)、在没有用户停止操作时让本来要接着做的一轮停下(`step.end` 只能多做或给结束换原因,`ui.stopSession` 只接受真实点击)、任意往会话里写东西或发起一轮(只允许自己的子会话和独立会话、钩子 `say` / `context`、宿主校验过的后台运行结果 `report`、工具经 `run.background()` 送回本会话的后台结果、工具经 `run.letter` 寄给另一个顶层会话的信(标着来自哪个会话)、`launch` 开出来的新会话的第一句话,以及自己的子会话里经 `run.parent` / `handToParent` 排进它父会话的任务;不能往别的已有会话里发、不能指定任意父会话或冒充用户授权)、没人点按钮时替人发送、绕过公开草稿接口操纵输入框、新增钩子或界面扩展点。
68
71
 
@@ -108,7 +111,7 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
108
111
  - README 里的图片:插件目录里的写相对路径(放在 `images/` 下,支持 PNG、JPEG、GIF、WebP、SVG,每张不超过 5 MiB),网上的写 https 地址;带深色版本的 `<picture>` 按窗口明暗选。
109
112
  - 根目录的 `icon.png` 会自动作为插件图标,不用配置;使用正方形 PNG,建议 192×192,文件不超过 1 MiB。不提供或图片损坏时显示默认标记。
110
113
  - `devDependencies` 只用于类型和打包:逻辑入口用到的第三方包全部打进 `dist/index.mjs`。
111
- - `dependencies` 只放打不进去的原生模块(如 `node-pty`):构建时连同实际安装的运行依赖、可选依赖和 peer 依赖一起复制进 `dist/node_modules`,只保留 Plume 发包的平台、架构(darwin、win32 的 arm64、x64,加上构建机)的包与预编译文件,随 `dist/` 一起装。
114
+ - `dependencies` 只放打不进去的原生模块(如 `node-pty`):构建时连同实际安装的运行依赖、可选依赖和 peer 依赖一起复制进 `dist/node_modules`,只保留 Plume 发包的平台、架构(darwin、win32 的 arm64、x64,加上构建机)的包与预编译文件,随 `dist/` 一起装。复制出的 Mach-O、ELF 文件(如 node-pty 的 `spawn-helper`)由构建补上执行位,不用为此写 postinstall。
112
115
  - 引入 `.css` 时加一个 `src/css.d.ts`(写法见 style-i18n.md)。
113
116
 
114
117
  ## 自检清单
@@ -125,7 +128,7 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
125
128
 
126
129
  ## 可以照着写的插件
127
130
 
128
- 都在 Plume 插件仓库(plume-plugins)的 `plugins/` 下,`todo`、`commands`、`web`、`skills`、`git` 随应用发、在本体仓库的 `packages/builtin/` 下(拿不到仓库时,以模板和本 skill 里的代码片段为准):
131
+ 都在 Plume 插件仓库(plume-plugins)的 `plugins/` 下,`todo`、`commands`、`web`、`skills`、`git`、`side-chat`、`subagents`、`plan` 随应用发、在本体仓库的 `packages/builtin/` 下(拿不到仓库时,以模板和本 skill 里的代码片段为准):
129
132
 
130
133
  | 插件 | 看它的什么 |
131
134
  | --- | --- |
@@ -137,14 +140,14 @@ description: 写、改、排查 Plume 插件:工具、钩子(tool.execute、
137
140
  | `usage` | 设置 › 数据里的一页(`ui.settingsSection` 的 `page: "nav"` 加 `group: "data"`,页头介绍用 `detail`);逻辑入口经 `ctx.model.usage` 读花销、`ctx.serve` 转给界面;SVG 自己画的堆叠柱状图(悬停气泡 fixed 定位)、环图(`stroke-dasharray` 分段)、热力图和色点,插件自带的模型品牌标 |
138
141
  | `screenshot` | 截屏浮层(`ui.screen`、`ui.screenOverlay`、`ui.pinnedShot`)、标注组件、输入框按钮(`ui.composerItem`)、占导航一行的设置分区(`ui.settingsSection` 的 `page: "nav"`)、逻辑入口跑子进程 |
139
142
  | `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` 切范围、表单弹窗 |
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` 切范围、表单弹窗 |
141
144
  | `im` | 不给 `page` 的设置分区:画进插件详情的「设置」一节,不占设置导航;密钥经 `ui.remote()` 交给逻辑入口存进 `ctx.dataDir`,界面只知道存没存 |
142
- | `index` | `inject` 服务 `project.files` 列项目文件;按会话缓存工具要用的数据、会话删掉时丢掉;设置分区用 `ui.useProjects` 列项目、经 `ui.remote()` 看状态、触发重建 |
145
+ | `index` | `ctx.workspace.files` 列项目文件;按会话缓存工具要用的数据、会话删掉时丢掉;设置分区用 `ui.useProjects` 列项目、经 `ui.remote()` 看状态、触发重建 |
143
146
  | `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` 菜单 |
147
+ | `mcp` | 运行中途增减工具:`ctx.tools.register` 返回的函数撤下单个工具;逻辑入口连外部服务、调用前 `run.ask`;大图存进 `ctx.media`、放进结果的 `shownImages` 由通用卡片画;服务要人填表时 `run.request` 带一句 `waiting`、卡片经 `ui.requestCard` 按 `kind` 登记;`model.context` 按估算的大小藏起工具、换成搜索入口;`ctx.sessions.onDeleted` 关按会话起的连接;`ctx.dataDir` 存配置和令牌,密钥不经界面;设置分区里的卡片(就地编辑、失焦保存)、弹窗和 `Popover` 菜单 |
145
148
  | `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`)列出批准过的计划、点了打开面板,面板从会话消息里读 |
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`)列出批准过的计划、点了打开面板,面板从会话消息里读 |
148
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/` |
149
- | `side-chat` | 面板里的多段子会话,`ui.Conversation` 带输入框;`prompt.submit` 给每一问附上主会话的增量,主会话的下一问附上侧边聊天改过的文件(`inject: ["file.changes"]` 认工具结果上的改动记录);`/btw` 自己办掉(`handled`、`openPanel`);排队消息的「在侧边聊天中打开」(`ui.queueAction`);关标签前用 `Confirm` 问一句;「会话」卡里一行选新侧边聊天的模型(`page: "session-models"`) |
152
+ | `side-chat` | 面板里的多段子会话,`ui.Conversation` 带输入框;`prompt.submit` 给每一问附上主会话的增量,主会话的下一问附上侧边聊天改过的文件(经 `ctx.changes` 认本体记在工具结果上的改动);`/btw` 自己办掉(`handled`、`openPanel`);排队消息的「在侧边聊天中打开」(`ui.queueAction`);关标签前用 `Confirm` 问一句;「会话」卡里一行选新侧边聊天的模型(`page: "session-models"`) |
150
153
  | `image` | 用设置里的生图模型出图(`ctx.model.image`),图存进媒体目录(`ctx.media`);工具卡片经进度推 JSON 行、边画边显示,`ui.mediaUrl` 显示、`ui.openImage` 看图和标注、`ui.draftInSession` 把图带回会话;`model.context` 没有生图模型时藏起工具;画廊页(`ui.page`)经 `ui.remote()` 扫全部会话、`ui.revealToolCall` 跳回那次调用 |
@@ -1327,6 +1327,23 @@ export function PageTopRow({
1327
1327
 
1328
1328
  ## 加载和占位
1329
1329
 
1330
+ ### `StatusSpinner`
1331
+
1332
+ 源码:`packages/client/ui/src/motion/loaders.tsx`
1333
+
1334
+ ```tsx
1335
+ /**
1336
+ * 一圈虚线,匀速转。
1337
+ *
1338
+ * 六段等分,所以转过 60° 就回到自身:没有可追踪的端点,眼睛抓不到锚点。这是它敢转、而一段
1339
+ * 弧不敢的原因——一段弧有头,十几个同时在屏幕上,每一个都在自己的相位上把视线勾一下。
1340
+ *
1341
+ * 颜色走 `currentColor`,默认跟着周围的文字走。要它说话大声一点的地方(任务清单里正在跑的
1342
+ * 那一步、流水线里排队的那一条)自己传 `text-accent`、`text-warning`。
1343
+ */
1344
+ export function StatusSpinner({ size = 14, className = "" }: { size?: number; className?: string })
1345
+ ```
1346
+
1330
1347
  ### `ActionSpinner`
1331
1348
 
1332
1349
  源码:`packages/client/ui/src/motion/loaders.tsx`