@coralai/sps-plugin-api 0.11.0 → 0.13.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/dist/index.d.ts +59 -39
- package/dist/index.js +61 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @coralai/sps-plugin-api —— **sps 宿主半区的插件契约**。
|
|
3
|
-
*
|
|
4
|
-
* 插件装它拿类型:`ctx['sps.tools']` 这些服务的方法签名。
|
|
5
|
-
*
|
|
6
|
-
* 🔴 **这个包是契约的真源,宿主去符合它** —— 不是反过来。
|
|
7
|
-
* 反过来(从宿主导出类型)的话,包永远滞后于实现,
|
|
8
|
-
* 而作者看到的类型和实际行为对不上时,他没法判断该信哪个。
|
|
9
|
-
* sps-cli 侧有一道一致性判据钉着:seam 与这里的接口对不上,**它自己的 check 会红**。
|
|
10
|
-
*
|
|
11
|
-
* ⚠️ 这里只声明**插件会调的那一面**。服务内部还有别的方法(比如注册表的
|
|
12
|
-
* `list()`),不写进来 —— 写进来就等于承诺它们不变,而那不是我们打算承诺的。
|
|
13
|
-
*/
|
|
14
1
|
export interface SpsToolSession {
|
|
15
2
|
cwd: string;
|
|
16
3
|
project?: string;
|
|
@@ -343,6 +330,16 @@ export interface SpsMemory {
|
|
|
343
330
|
project?: string;
|
|
344
331
|
all?: boolean;
|
|
345
332
|
}): MemoryMigrateStats;
|
|
333
|
+
/**
|
|
334
|
+
* 要注入 worker 的 Claude Code 插件目录(**绝对路径**)。没有就返回 `[]`。
|
|
335
|
+
*
|
|
336
|
+
* 用于把"自动召回"做进会话:hooks 在 worker 的 claude 进程里跑,
|
|
337
|
+
* 不依赖 agent 主动调工具。
|
|
338
|
+
*
|
|
339
|
+
* ⚠️ 只有 claude 后端会用到 —— 别的 agent(openai/codex)没有对应概念,
|
|
340
|
+
* 它们要等价能力时各自在自己的适配器里解决。
|
|
341
|
+
*/
|
|
342
|
+
workerPlugins(): string[];
|
|
346
343
|
}
|
|
347
344
|
export interface MemoryGcStats {
|
|
348
345
|
/** 硬删的 tombstone 数。 */
|
|
@@ -354,6 +351,53 @@ export interface MemoryGcStats {
|
|
|
354
351
|
}
|
|
355
352
|
/** 迁移了多少条 —— 键由后端自定(本地实现给 global/agents/projects)。 */
|
|
356
353
|
export type MemoryMigrateStats = Record<string, number>;
|
|
354
|
+
export interface SpsWorkspaceProject {
|
|
355
|
+
/** 相对工作区根,如 `t<租户>/<产品>/<项目>`。 */
|
|
356
|
+
rel: string;
|
|
357
|
+
/** 绝对路径。 */
|
|
358
|
+
abs: string;
|
|
359
|
+
/** 布局第二段 —— 资产声明按它取。 */
|
|
360
|
+
product: string;
|
|
361
|
+
}
|
|
362
|
+
export interface SpsAssetScope {
|
|
363
|
+
/** 相对项目根的路径前缀。 */
|
|
364
|
+
include: string[];
|
|
365
|
+
/**
|
|
366
|
+
* 有没有用兜底。
|
|
367
|
+
* ⚠️ **这一格必须能被报出来** —— "范围内什么都没有"和"根本没声明范围"
|
|
368
|
+
* 读数一样,处置完全不同。
|
|
369
|
+
*/
|
|
370
|
+
usedDefault: boolean;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* 工作区布局(**只读**)。给需要遍历用户产物的插件用(如同步到对象存储)。
|
|
374
|
+
*
|
|
375
|
+
* ⚠️ `discover()` 与 `sps.projects.list()` 是**两个不同的集合**:
|
|
376
|
+
* 前者按目录布局扫,后者读注册表。目录里有而没注册的项目只出现在前者。
|
|
377
|
+
*/
|
|
378
|
+
export interface SpsWorkspace {
|
|
379
|
+
/** 工作区根(绝对路径)。 */
|
|
380
|
+
root(): string;
|
|
381
|
+
/** 按布局约定发现项目。 */
|
|
382
|
+
discover(): Promise<SpsWorkspaceProject[]>;
|
|
383
|
+
/** 某产品的用户资产范围。没有声明时回落窄兜底,并把 `usedDefault` 置真。 */
|
|
384
|
+
assetScope(product: string): SpsAssetScope;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* `p` 是否在 `root` 之内(含 root 自身)。
|
|
388
|
+
*
|
|
389
|
+
* 🔴 **必须比到分隔符**:裸 `startsWith(root)` 会把 `<root>-old` 当成 root 内。
|
|
390
|
+
*/
|
|
391
|
+
export declare function isUnderRoot(p: string, root: string): boolean;
|
|
392
|
+
/**
|
|
393
|
+
* 把路径解析到**真实位置**(软链解开)。
|
|
394
|
+
* 目标可能尚不存在(输出文件),故解析其**父目录**的 realpath 再拼回文件名。
|
|
395
|
+
*
|
|
396
|
+
* ⚠️ **异步**:同步 fs 会阻塞事件循环(含健康探针与 SSE 心跳)。
|
|
397
|
+
*/
|
|
398
|
+
export declare function realResolve(p: string): Promise<string>;
|
|
399
|
+
/** realpath 之后再判围栏。**这是唯一该被安全路径调用的那个**;字符串比较不算围栏。 */
|
|
400
|
+
export declare function isRealUnderRoot(p: string, root: string): Promise<boolean>;
|
|
357
401
|
/** 写入被脱敏规则丢弃时,后端抛出的错误应带这个 code。宿主据此与真错误区分。 */
|
|
358
402
|
export declare const MEMORY_REDACTED_CODE = "memory-redacted";
|
|
359
403
|
/** 去重归一化:小写、去 markdown 标点、压空白。**这是"什么算重复"的唯一判据**。 */
|
|
@@ -374,30 +418,6 @@ export interface SpsNotifier {
|
|
|
374
418
|
send(message: string, level?: 'info' | 'success' | 'warning' | 'error'): Promise<void>;
|
|
375
419
|
};
|
|
376
420
|
}
|
|
377
|
-
export interface SpsStorageObject {
|
|
378
|
-
key: string;
|
|
379
|
-
/** **内容判据**(单段上传时等于 md5)。不看时间。 */
|
|
380
|
-
etag: string;
|
|
381
|
-
size: number;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* 对象存储(读 / 列 / 写)。桶是共享面 —— 多节点下 studio 和工作区不在同一台机器上。
|
|
385
|
-
*
|
|
386
|
-
* 🔴 **它只搬字节,不做账。** id / 索引 / 缩略图 / 分类 / 配额 / 计费都在平台侧。
|
|
387
|
-
*
|
|
388
|
-
* ⚠️ **权限按前缀分,而且很紧**(单写方规则)。
|
|
389
|
-
* 读不了的前缀会**抛**,不会回空 —— "库是空的"和"我没权限"混成一个,
|
|
390
|
-
* 人会去补素材而不是去补策略。
|
|
391
|
-
* ⚠️ 桶没配时三个方法都抛 ⇒ **先用 `configured()` 问一句**,别拿异常当分支。
|
|
392
|
-
*/
|
|
393
|
-
export interface SpsStorage {
|
|
394
|
-
configured(): boolean;
|
|
395
|
-
/** 不存在 ⇒ `null`;连不上/没权限 ⇒ **抛**。 */
|
|
396
|
-
get(key: string): Promise<Buffer | null>;
|
|
397
|
-
list(prefix: string): Promise<SpsStorageObject[]>;
|
|
398
|
-
/** ⚠️ `contentType` 自己决定 —— 桶是直连的,缺省会让预览变成一次下载。 */
|
|
399
|
-
put(key: string, bytes: Buffer, contentType?: string): Promise<void>;
|
|
400
|
-
}
|
|
401
421
|
/** 与 `SpsAsyncPoll` 同形 —— async 能力的 `poll` 可以**直接透传**。 */
|
|
402
422
|
export type SpsJobStatus = SpsAsyncPoll;
|
|
403
423
|
/**
|
|
@@ -623,7 +643,6 @@ export declare const SPS_SERVICES: {
|
|
|
623
643
|
readonly projects: "sps.projects";
|
|
624
644
|
readonly cards: "sps.cards";
|
|
625
645
|
readonly jobs: "sps.jobs";
|
|
626
|
-
readonly storage: "sps.storage";
|
|
627
646
|
readonly subprocess: "sps.subprocess";
|
|
628
647
|
readonly attachments: "sps.attachments";
|
|
629
648
|
readonly media: "sps.media";
|
|
@@ -632,6 +651,7 @@ export declare const SPS_SERVICES: {
|
|
|
632
651
|
readonly agents: "sps.agents";
|
|
633
652
|
readonly repo: "sps.repo";
|
|
634
653
|
readonly memory: "sps.memory";
|
|
654
|
+
readonly workspace: "sps.workspace";
|
|
635
655
|
readonly notifier: "sps.notifier";
|
|
636
656
|
};
|
|
637
657
|
/** 插件 ctx 上能拿到的服务(只列已声明契约的那几个)。 */
|
|
@@ -646,13 +666,13 @@ export interface SpsPluginContext {
|
|
|
646
666
|
'sps.projects': SpsProjects;
|
|
647
667
|
'sps.cards': SpsCards;
|
|
648
668
|
'sps.jobs': SpsJobs;
|
|
649
|
-
'sps.storage': SpsStorage;
|
|
650
669
|
'sps.capabilities': SpsCapabilities;
|
|
651
670
|
'sps.media': SpsMedia;
|
|
652
671
|
'sps.im': SpsRegistryByKey;
|
|
653
672
|
'sps.agents': SpsRegistryByFactory;
|
|
654
673
|
'sps.repo': SpsRegistryByFactory;
|
|
655
674
|
'sps.memory': SpsMemory;
|
|
675
|
+
'sps.workspace': SpsWorkspace;
|
|
656
676
|
'sps.notifier': SpsNotifier;
|
|
657
677
|
'sps.subprocess': SpsSubprocess;
|
|
658
678
|
'sps.attachments': SpsAttachments;
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,66 @@
|
|
|
11
11
|
* ⚠️ 这里只声明**插件会调的那一面**。服务内部还有别的方法(比如注册表的
|
|
12
12
|
* `list()`),不写进来 —— 写进来就等于承诺它们不变,而那不是我们打算承诺的。
|
|
13
13
|
*/
|
|
14
|
+
import { realpath } from 'node:fs/promises';
|
|
15
|
+
import { dirname, join, resolve as nodeResolve, sep } from 'node:path';
|
|
16
|
+
// ── 路径围栏:所有插件共用的安全下限 ──────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// 🔴 **安全判据不能有第二份。** 本仓已经为这个形状付过三次代价:
|
|
19
|
+
// `providers/art/withinDir.ts`(字符串,理智检查)与 `console/lib/artPaths.ts`
|
|
20
|
+
// (realpath,安全边界)长得几乎一样、靠头注区分;`scan.ts` 用 Dirent 做对了、
|
|
21
|
+
// 隔壁用 `stat`(跟随软链)就漏了。
|
|
22
|
+
// > **一条安全规则做对过一次,不代表它在第二条路上也在** ——
|
|
23
|
+
// > 而两条路写法不同时,正是它只做一半的时候。
|
|
24
|
+
// ⇒ 插件要围栏时**从这里取**,别自己写第四份。
|
|
25
|
+
//
|
|
26
|
+
// ⚠️ 本节**只做围栏**,不回答"这个文件该不该同步/发布/服务" ——
|
|
27
|
+
// 那三者判据不同、各自命名。合并会制造"它们本该一致"的错觉,
|
|
28
|
+
// 而那个错觉比漂移本身更危险:下一个人会去"修复不一致",把三条用途不同的规则强行统一。
|
|
29
|
+
/**
|
|
30
|
+
* `p` 是否在 `root` 之内(含 root 自身)。
|
|
31
|
+
*
|
|
32
|
+
* 🔴 **必须比到分隔符**:裸 `startsWith(root)` 会把 `<root>-old` 当成 root 内。
|
|
33
|
+
*/
|
|
34
|
+
export function isUnderRoot(p, root) {
|
|
35
|
+
const r = nodeResolve(root);
|
|
36
|
+
const t = nodeResolve(p);
|
|
37
|
+
return t === r || t.startsWith(r + sep);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 把路径解析到**真实位置**(软链解开)。
|
|
41
|
+
* 目标可能尚不存在(输出文件),故解析其**父目录**的 realpath 再拼回文件名。
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ **异步**:同步 fs 会阻塞事件循环(含健康探针与 SSE 心跳)。
|
|
44
|
+
*/
|
|
45
|
+
export async function realResolve(p) {
|
|
46
|
+
const abs = nodeResolve(p);
|
|
47
|
+
try {
|
|
48
|
+
return await realpath(abs);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
try {
|
|
52
|
+
return join(await realpath(dirname(abs)), abs.slice(dirname(abs).length + 1));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return abs;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** realpath 之后再判围栏。**这是唯一该被安全路径调用的那个**;字符串比较不算围栏。 */
|
|
60
|
+
export async function isRealUnderRoot(p, root) {
|
|
61
|
+
return isUnderRoot(await realResolve(p), await realResolve(root));
|
|
62
|
+
}
|
|
63
|
+
// ── 后端可以往 worker 注入一个 Claude Code 插件 ─────────────────────────────
|
|
64
|
+
//
|
|
65
|
+
// 记忆的读路有两半:**pull**(agent 自己调 `memory_recall`)与 **push**(会话自动召回)。
|
|
66
|
+
// 只有 pull 时,工具挂着而 agent 不调 = 等于没记忆 —— 这是实测过的失效模式。
|
|
67
|
+
// push 那一半靠 Claude Code 的 hooks,而 hooks 装在一个 claude 插件目录里。
|
|
68
|
+
//
|
|
69
|
+
// 🔴 宿主**只要一个目录路径**,不解析里面的 hooks.json / .mcp.json ——
|
|
70
|
+
// 那是运行时 substrate 的格式,和"sps 不解析 claude 的 transcript"同源。
|
|
71
|
+
// ⚠️ 目录由 SDK 的 `plugins: [{type:'local', path}]` 加载,**不需要装进
|
|
72
|
+
// `~/.claude/plugins`**(2026-08-27 实测:任意路径可加载,MCP server 正常注册)。
|
|
73
|
+
// 这也意味着关掉 = 不传参数,零残留。
|
|
14
74
|
// ── 跨后端必须一致的算法 ──────────────────────────────────────────────────
|
|
15
75
|
//
|
|
16
76
|
// 🔴 放这里的判据只有一条:**两个后端对它给出不同答案会出错**。
|
|
@@ -68,7 +128,6 @@ export const SPS_SERVICES = {
|
|
|
68
128
|
projects: 'sps.projects',
|
|
69
129
|
cards: 'sps.cards',
|
|
70
130
|
jobs: 'sps.jobs',
|
|
71
|
-
storage: 'sps.storage',
|
|
72
131
|
subprocess: 'sps.subprocess',
|
|
73
132
|
attachments: 'sps.attachments',
|
|
74
133
|
media: 'sps.media',
|
|
@@ -77,5 +136,6 @@ export const SPS_SERVICES = {
|
|
|
77
136
|
agents: 'sps.agents',
|
|
78
137
|
repo: 'sps.repo',
|
|
79
138
|
memory: 'sps.memory',
|
|
139
|
+
workspace: 'sps.workspace',
|
|
80
140
|
notifier: 'sps.notifier',
|
|
81
141
|
};
|