@capybara-hq/plugin-sdk 0.0.0-stage → 0.1.0-rc.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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +27 -2
- package/dist/agent-template.d.ts +47 -0
- package/dist/agent-template.js +123 -0
- package/dist/bootstrap.d.ts +67 -0
- package/dist/bootstrap.js +109 -0
- package/dist/cards.d.ts +64 -0
- package/dist/cards.js +217 -0
- package/dist/channel.d.ts +230 -0
- package/dist/channel.js +26 -0
- package/dist/connector-auth.d.ts +64 -0
- package/dist/connector-auth.js +96 -0
- package/dist/connector.d.ts +154 -0
- package/dist/connector.js +1 -0
- package/dist/ctx.d.ts +176 -0
- package/dist/ctx.js +1 -0
- package/dist/define-extension.d.ts +41 -0
- package/dist/define-extension.js +85 -0
- package/dist/directory.d.ts +29 -0
- package/dist/directory.js +1 -0
- package/dist/extension-point.d.ts +29 -0
- package/dist/extension-point.js +36 -0
- package/dist/file-preview.d.ts +64 -0
- package/dist/file-preview.js +72 -0
- package/dist/frontmatter.d.ts +12 -0
- package/dist/frontmatter.js +36 -0
- package/dist/hooks.d.ts +194 -0
- package/dist/hooks.js +14 -0
- package/dist/identity.d.ts +44 -0
- package/dist/identity.js +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/marketplace.d.ts +99 -0
- package/dist/marketplace.js +256 -0
- package/dist/model-params.d.ts +82 -0
- package/dist/model-params.js +222 -0
- package/dist/model.d.ts +227 -0
- package/dist/model.js +152 -0
- package/dist/permissions.d.ts +67 -0
- package/dist/permissions.js +271 -0
- package/dist/plugin-manifest.d.ts +141 -0
- package/dist/plugin-manifest.js +322 -0
- package/dist/plugin-package.d.ts +139 -0
- package/dist/plugin-package.js +448 -0
- package/dist/sandbox.d.ts +152 -0
- package/dist/sandbox.js +96 -0
- package/dist/secret.d.ts +5 -0
- package/dist/secret.js +8 -0
- package/dist/semver-range.d.ts +6 -0
- package/dist/semver-range.js +12 -0
- package/dist/skill-invocation.d.ts +83 -0
- package/dist/skill-invocation.js +201 -0
- package/dist/skill-keys.d.ts +9 -0
- package/dist/skill-keys.js +16 -0
- package/dist/skill.d.ts +77 -0
- package/dist/skill.js +244 -0
- package/dist/storage.d.ts +96 -0
- package/dist/storage.js +9 -0
- package/dist/tool.d.ts +154 -0
- package/dist/tool.js +218 -0
- package/dist/ui.d.ts +265 -0
- package/dist/ui.js +358 -0
- package/dist/url.d.ts +2 -0
- package/dist/url.js +12 -0
- package/package.json +40 -3
- package/schemas/bootstrap.v1.schema.json +125 -0
- package/schemas/connector-template.v1.schema.json +116 -0
- package/schemas/marketplace.v1.schema.json +270 -0
- package/schemas/plugin.v1.schema.json +330 -0
package/dist/skill.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能(Agent Skills 格式)的纯解析与校验函数:内核上传技能包、agent 提议技能改动时都用它们。
|
|
3
|
+
*
|
|
4
|
+
* 格式依据(2026-10-21 核实):
|
|
5
|
+
* - Agent Skills 规范 https://agentskills.io/specification :`name` 必填,1–64 个字符,只能是小写字母、数字与
|
|
6
|
+
* 连字符,不能以连字符开头或结尾,不能有连续连字符,须与所在目录同名;`description` 必填,1–1024 个字符;
|
|
7
|
+
* 可选 `license`、`compatibility`(1–500 个字符)、`metadata`、`allowed-tools`。
|
|
8
|
+
* - Anthropic 文档 https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview :
|
|
9
|
+
* `name` 与 `description` 不能含 XML 标签(另有保留词 "anthropic" / "claude",那是 Anthropic 平台自己的规则,
|
|
10
|
+
* 这里不采用)。
|
|
11
|
+
*
|
|
12
|
+
* Choices: the description goes into the model's system prompt, so XML tags are refused as Anthropic
|
|
13
|
+
* does (no forged prompt structure). Keys outside the specification are allowed and ignored, so
|
|
14
|
+
* existing packages upload unchanged, except Claude Code's invocation keys
|
|
15
|
+
* (`disable-model-invocation`, `user-invocable`, `context`, `agent`, `argument-hint`, `arguments`),
|
|
16
|
+
* whose types are checked because the kernel uses them (`skillInvocation`, phase D). Matching the
|
|
17
|
+
* directory name is not enforced: an uploaded zip's outer directory often has another name (GitHub's
|
|
18
|
+
* `repo-main/`); the skill's name is always the frontmatter `name`, and its sandbox directory is
|
|
19
|
+
* `/skills/<name>/`.
|
|
20
|
+
*/
|
|
21
|
+
import { readFrontmatter } from "./frontmatter.js";
|
|
22
|
+
import { hasControlChars } from "./model.js";
|
|
23
|
+
import { validateSandboxPath } from "./sandbox.js";
|
|
24
|
+
import { argumentHintText, SKILL_ARGUMENT_NAME_RE } from "./skill-keys.js";
|
|
25
|
+
/** 技能包的上限。 */
|
|
26
|
+
export const SKILL_LIMITS = {
|
|
27
|
+
nameMaxChars: 64,
|
|
28
|
+
descriptionMaxChars: 1024,
|
|
29
|
+
compatibilityMaxChars: 500,
|
|
30
|
+
/** 单个文件解压后的上限(字节)。 */
|
|
31
|
+
fileBytes: 10 * 1024 * 1024,
|
|
32
|
+
/** 全部文件解压后的总量上限(字节)。 */
|
|
33
|
+
totalBytes: 50 * 1024 * 1024,
|
|
34
|
+
/** 文件数上限。 */
|
|
35
|
+
maxFiles: 1000,
|
|
36
|
+
/** 包内相对路径的长度上限(字符)。 */
|
|
37
|
+
pathMaxChars: 512,
|
|
38
|
+
/** frontmatter(两条 `---` 之间)的长度上限(字符),超过不做 YAML 解析。 */
|
|
39
|
+
frontmatterMaxChars: 64 * 1024,
|
|
40
|
+
};
|
|
41
|
+
/** Limits of the invocation keys of a SKILL.md (see `SkillInvocation`). */
|
|
42
|
+
export const SKILL_INVOCATION_LIMITS = {
|
|
43
|
+
/** `argument-hint`, in characters. */
|
|
44
|
+
argumentHintMaxChars: 200,
|
|
45
|
+
/** Names in `arguments`. */
|
|
46
|
+
argumentNames: 20,
|
|
47
|
+
/** The argument string of one invocation, in characters (after trimming). */
|
|
48
|
+
argumentsMaxChars: 10_000,
|
|
49
|
+
/**
|
|
50
|
+
* The rendered body of one invocation (substitutions and the `ARGUMENTS:` line), in UTF-16 code
|
|
51
|
+
* units: placeholders copy the arguments again for every occurrence, so this bounds the output.
|
|
52
|
+
*/
|
|
53
|
+
renderedMaxChars: 200_000,
|
|
54
|
+
};
|
|
55
|
+
/** 技能入口文件(包根目录,大小写敏感)。 */
|
|
56
|
+
export const SKILL_ENTRY_FILE = "SKILL.md";
|
|
57
|
+
/** 沙箱里放技能的根目录:每个技能在 `/skills/<name>/` 下(只读复制)。 */
|
|
58
|
+
export const SKILLS_SANDBOX_ROOT = "/skills";
|
|
59
|
+
const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
60
|
+
const XML_TAG_RE = /<\/?[A-Za-z][^<>]*>/;
|
|
61
|
+
const DRIVE_RE = /^[A-Za-z]:/;
|
|
62
|
+
/** 是否为合法的技能名(见文件头的规范说明)。 */
|
|
63
|
+
export function isSkillName(s) {
|
|
64
|
+
return typeof s === "string" && s.length <= SKILL_LIMITS.nameMaxChars && NAME_RE.test(s);
|
|
65
|
+
}
|
|
66
|
+
/** 技能文件在沙箱里的路径:`/skills/<name>/<path>`。 */
|
|
67
|
+
export function skillSandboxPath(name, path) {
|
|
68
|
+
return `${SKILLS_SANDBOX_ROOT}/${name}/${path}`;
|
|
69
|
+
}
|
|
70
|
+
const chars = (s) => [...s].length;
|
|
71
|
+
/**
|
|
72
|
+
* 解析 SKILL.md:开头是 `---` 行包住的 YAML frontmatter,之后是正文。容忍 BOM 与 CRLF(正文里的 CRLF 换成 LF)。
|
|
73
|
+
* 返回 name / description / 正文,或全部问题(英文,可直接给模型或管理员看)。
|
|
74
|
+
*/
|
|
75
|
+
export function parseSkillMd(text, options = {}) {
|
|
76
|
+
const read = readFrontmatter(text, "SKILL.md", SKILL_LIMITS.frontmatterMaxChars);
|
|
77
|
+
if ("errors" in read)
|
|
78
|
+
return read;
|
|
79
|
+
const { frontmatter: fm, body } = read;
|
|
80
|
+
const errors = [];
|
|
81
|
+
const name = fm.name;
|
|
82
|
+
if (name === undefined || name === null)
|
|
83
|
+
errors.push("name is required");
|
|
84
|
+
else if (typeof name !== "string")
|
|
85
|
+
errors.push("name must be a string");
|
|
86
|
+
else if (!isSkillName(name)) {
|
|
87
|
+
errors.push("name must be 1-64 lowercase letters, digits and single hyphens, not starting or ending with a hyphen");
|
|
88
|
+
}
|
|
89
|
+
const rawDesc = fm.description;
|
|
90
|
+
let description = "";
|
|
91
|
+
if (rawDesc === undefined || rawDesc === null)
|
|
92
|
+
errors.push("description is required");
|
|
93
|
+
else if (typeof rawDesc !== "string")
|
|
94
|
+
errors.push("description must be a string");
|
|
95
|
+
else {
|
|
96
|
+
description = rawDesc.trim();
|
|
97
|
+
if (description === "")
|
|
98
|
+
errors.push("description must not be empty");
|
|
99
|
+
else if (chars(description) > SKILL_LIMITS.descriptionMaxChars) {
|
|
100
|
+
errors.push(`description must be at most ${SKILL_LIMITS.descriptionMaxChars} characters`);
|
|
101
|
+
}
|
|
102
|
+
else if (XML_TAG_RE.test(description)) {
|
|
103
|
+
errors.push("description must not contain XML tags");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const compat = fm.compatibility;
|
|
107
|
+
if (compat !== undefined &&
|
|
108
|
+
(typeof compat !== "string" ||
|
|
109
|
+
compat.length === 0 ||
|
|
110
|
+
chars(compat) > SKILL_LIMITS.compatibilityMaxChars)) {
|
|
111
|
+
errors.push(`compatibility must be a string of 1-${SKILL_LIMITS.compatibilityMaxChars} characters`);
|
|
112
|
+
}
|
|
113
|
+
if (options.invocationKeys !== false)
|
|
114
|
+
errors.push(...invocationKeyErrors(fm));
|
|
115
|
+
if (errors.length > 0)
|
|
116
|
+
return { errors };
|
|
117
|
+
return { name: name, description, body, frontmatter: fm };
|
|
118
|
+
}
|
|
119
|
+
const AGENT_MAX_CHARS = 130;
|
|
120
|
+
/** The type errors of the invocation keys (`disable-model-invocation`, `context`, ...). */
|
|
121
|
+
function invocationKeyErrors(fm) {
|
|
122
|
+
const errors = [];
|
|
123
|
+
const dmi = fm["disable-model-invocation"];
|
|
124
|
+
if (dmi !== undefined && typeof dmi !== "boolean") {
|
|
125
|
+
errors.push("disable-model-invocation must be a boolean");
|
|
126
|
+
}
|
|
127
|
+
const ui = fm["user-invocable"];
|
|
128
|
+
if (ui !== undefined && typeof ui !== "boolean")
|
|
129
|
+
errors.push("user-invocable must be a boolean");
|
|
130
|
+
if (fm.context !== undefined && fm.context !== "fork")
|
|
131
|
+
errors.push('context must be "fork"');
|
|
132
|
+
const agent = fm.agent;
|
|
133
|
+
if (agent !== undefined &&
|
|
134
|
+
(typeof agent !== "string" || agent === "" || chars(agent) > AGENT_MAX_CHARS)) {
|
|
135
|
+
errors.push(`agent must be a string of 1-${AGENT_MAX_CHARS} characters`);
|
|
136
|
+
}
|
|
137
|
+
const hint = fm["argument-hint"];
|
|
138
|
+
if (hint !== undefined) {
|
|
139
|
+
const text = argumentHintText(hint);
|
|
140
|
+
if (text === undefined || chars(text) > SKILL_INVOCATION_LIMITS.argumentHintMaxChars) {
|
|
141
|
+
errors.push(`argument-hint must be a string of at most ${SKILL_INVOCATION_LIMITS.argumentHintMaxChars} characters`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const names = fm.arguments;
|
|
145
|
+
if (names !== undefined &&
|
|
146
|
+
(!Array.isArray(names) ||
|
|
147
|
+
names.length > SKILL_INVOCATION_LIMITS.argumentNames ||
|
|
148
|
+
!names.every((n) => typeof n === "string" && SKILL_ARGUMENT_NAME_RE.test(n)))) {
|
|
149
|
+
errors.push(`arguments must be a list of at most ${SKILL_INVOCATION_LIMITS.argumentNames} names`);
|
|
150
|
+
}
|
|
151
|
+
if (dmi === true && ui === false) {
|
|
152
|
+
errors.push("a skill with disable-model-invocation: true and user-invocable: false cannot be used");
|
|
153
|
+
}
|
|
154
|
+
return errors;
|
|
155
|
+
}
|
|
156
|
+
/** 给人看的路径:含控制字符时用 JSON 转义。 */
|
|
157
|
+
const show = (p) => (hasControlChars(p) || p === "" ? JSON.stringify(p) : p);
|
|
158
|
+
/** 单个相对路径的问题(第一个),合法时为 null。 */
|
|
159
|
+
function pathProblem(p) {
|
|
160
|
+
if (typeof p !== "string" || p === "")
|
|
161
|
+
return "path must be a non-empty string";
|
|
162
|
+
if (chars(p) > SKILL_LIMITS.pathMaxChars) {
|
|
163
|
+
return `path must be at most ${SKILL_LIMITS.pathMaxChars} characters`;
|
|
164
|
+
}
|
|
165
|
+
if (hasControlChars(p))
|
|
166
|
+
return "path must not contain control characters";
|
|
167
|
+
if (p.includes("\\"))
|
|
168
|
+
return "path must not contain backslashes";
|
|
169
|
+
if (p.startsWith("/"))
|
|
170
|
+
return "path must be relative";
|
|
171
|
+
if (DRIVE_RE.test(p))
|
|
172
|
+
return "path must not start with a drive letter";
|
|
173
|
+
if (p.endsWith("/"))
|
|
174
|
+
return "path must name a file, not a directory";
|
|
175
|
+
for (const seg of p.split("/")) {
|
|
176
|
+
if (seg === "")
|
|
177
|
+
return "path must not contain empty segments";
|
|
178
|
+
if (seg === "." || seg === "..")
|
|
179
|
+
return "path must not contain '.' or '..' segments";
|
|
180
|
+
}
|
|
181
|
+
// 再按沙箱路径规则核一遍(百分号编码的 `..` 等)
|
|
182
|
+
const errs = validateSandboxPath(skillSandboxPath("skill", p));
|
|
183
|
+
return errs[0] ?? null;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 校验技能包的文件清单(路径相对于包根目录,只含文件、不含目录项):
|
|
187
|
+
* 根目录必须有 `SKILL.md`;单文件 ≤ {@link SKILL_LIMITS.fileBytes};总量 ≤ {@link SKILL_LIMITS.totalBytes};
|
|
188
|
+
* 文件数 ≤ {@link SKILL_LIMITS.maxFiles};路径不能越界(绝对路径、`..`、反斜杠、盘符、控制字符),不能重复
|
|
189
|
+
* (只差大小写也算重复:大小写不敏感的文件系统上会互相覆盖),文件不能与目录同名。返回问题列表,空数组表示合法。
|
|
190
|
+
*/
|
|
191
|
+
export function validateSkillPackage(files) {
|
|
192
|
+
if (files.length > SKILL_LIMITS.maxFiles) {
|
|
193
|
+
return [`package has more than ${SKILL_LIMITS.maxFiles} files`];
|
|
194
|
+
}
|
|
195
|
+
const errors = [];
|
|
196
|
+
if (!files.some((f) => f.path === SKILL_ENTRY_FILE)) {
|
|
197
|
+
errors.push(`${SKILL_ENTRY_FILE} is required at the root`);
|
|
198
|
+
}
|
|
199
|
+
const exact = new Set();
|
|
200
|
+
const folded = new Set();
|
|
201
|
+
const fileKeys = new Set();
|
|
202
|
+
const dirKeys = new Set();
|
|
203
|
+
let total = 0;
|
|
204
|
+
for (const f of files) {
|
|
205
|
+
const p = f.path;
|
|
206
|
+
const problem = pathProblem(p);
|
|
207
|
+
if (problem) {
|
|
208
|
+
errors.push(`${show(String(p))}: ${problem}`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!Number.isSafeInteger(f.size) || f.size < 0) {
|
|
212
|
+
errors.push(`${show(p)}: size must be a non-negative integer`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (f.size > SKILL_LIMITS.fileBytes) {
|
|
216
|
+
errors.push(`${show(p)}: file is larger than ${SKILL_LIMITS.fileBytes} bytes`);
|
|
217
|
+
}
|
|
218
|
+
total += f.size;
|
|
219
|
+
const key = p.normalize("NFC").toLowerCase();
|
|
220
|
+
if (exact.has(p)) {
|
|
221
|
+
errors.push(`${show(p)}: duplicate path`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
exact.add(p);
|
|
225
|
+
if (folded.has(key)) {
|
|
226
|
+
errors.push(`${show(p)}: duplicate path (paths differ only in letter case)`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
folded.add(key);
|
|
230
|
+
const segs = key.split("/");
|
|
231
|
+
const parents = segs.slice(0, -1).map((_, i) => segs.slice(0, i + 1).join("/"));
|
|
232
|
+
if (dirKeys.has(key) || parents.some((d) => fileKeys.has(d))) {
|
|
233
|
+
errors.push(`${show(p)}: a file and a directory have the same path`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
fileKeys.add(key);
|
|
237
|
+
for (const d of parents)
|
|
238
|
+
dirKeys.add(d);
|
|
239
|
+
}
|
|
240
|
+
if (total > SKILL_LIMITS.totalBytes) {
|
|
241
|
+
errors.push(`package is larger than ${SKILL_LIMITS.totalBytes} bytes in total`);
|
|
242
|
+
}
|
|
243
|
+
return errors;
|
|
244
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object storage contract `object-store@1`: a single-select, brokered extension point (extensions
|
|
3
|
+
* may not `requires` it; they use it through `ctx.storage`). The provider implements
|
|
4
|
+
* {@link ObjectStore}; the `ctx.storage` an extension gets ({@link ExtensionStorage}) is wrapped by
|
|
5
|
+
* the kernel: every method takes a leading `orgId`, and keys get the prefix
|
|
6
|
+
* `plugins/<extension name>/<orgId>/`.
|
|
7
|
+
*/
|
|
8
|
+
/** Object storage extension point ref. */
|
|
9
|
+
export declare const OBJECT_STORE_EXTENSION_POINT = "object-store@1";
|
|
10
|
+
/** 对象的元数据。 */
|
|
11
|
+
export interface ObjectHead {
|
|
12
|
+
key: string;
|
|
13
|
+
/** 字节数。 */
|
|
14
|
+
size: number;
|
|
15
|
+
contentType: string;
|
|
16
|
+
etag?: string;
|
|
17
|
+
/** ISO 8601 时间。 */
|
|
18
|
+
lastModified?: string;
|
|
19
|
+
}
|
|
20
|
+
/** `object-store@1` 的实现(由对象存储插件提供,如 S3 兼容存储)。 */
|
|
21
|
+
export interface ObjectStore {
|
|
22
|
+
/** 写入(覆盖同名键);`body` 是流时最好给出 `size`。 */
|
|
23
|
+
put(key: string, body: Uint8Array | ReadableStream<Uint8Array>, opts: {
|
|
24
|
+
contentType: string;
|
|
25
|
+
size?: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}): Promise<ObjectHead>;
|
|
28
|
+
/**
|
|
29
|
+
* 读取;不存在返回 null。`range` 为闭区间字节范围,缺省 `end` 表示读到末尾。
|
|
30
|
+
* 带 `range` 时 `head.size` 仍是整个对象的大小;存储忽略 Range 时 body 可能是整个对象。
|
|
31
|
+
*/
|
|
32
|
+
get(key: string, opts?: {
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
range?: {
|
|
35
|
+
start: number;
|
|
36
|
+
end?: number;
|
|
37
|
+
};
|
|
38
|
+
}): Promise<{
|
|
39
|
+
head: ObjectHead;
|
|
40
|
+
body: ReadableStream<Uint8Array>;
|
|
41
|
+
} | null>;
|
|
42
|
+
/** 不存在返回 null。 */
|
|
43
|
+
head(key: string): Promise<ObjectHead | null>;
|
|
44
|
+
/** 删除;不存在也算成功。 */
|
|
45
|
+
delete(key: string): Promise<void>;
|
|
46
|
+
/** 删除前缀下的全部对象,返回删除数(用于组织数据删除)。 */
|
|
47
|
+
deletePrefix(prefix: string): Promise<number>;
|
|
48
|
+
/**
|
|
49
|
+
* 分页列举前缀下的对象;返回的 `cursor` 缺省表示没有下一页。列举结果不带类型时 `contentType` 为空串
|
|
50
|
+
* (需要时用 `head` 取)。
|
|
51
|
+
*/
|
|
52
|
+
list(prefix: string, opts?: {
|
|
53
|
+
cursor?: string;
|
|
54
|
+
limit?: number;
|
|
55
|
+
}): Promise<{
|
|
56
|
+
items: ObjectHead[];
|
|
57
|
+
cursor?: string;
|
|
58
|
+
}>;
|
|
59
|
+
/** 预签名下载 URL(选项见 {@link PresignGetOptions})。 */
|
|
60
|
+
presignGet(key: string, opts: PresignGetOptions): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* 预签名上传:浏览器按返回的 {@link PresignedUpload} 直传存储。超过 `maxSize` 的上传必须由**存储端**拒绝
|
|
63
|
+
* (预签名 PUT 无法限制大小,所以 S3 兼容实现用带 `content-length-range` 条件的 POST 策略)。
|
|
64
|
+
*/
|
|
65
|
+
presignPut(key: string, opts: {
|
|
66
|
+
contentType: string;
|
|
67
|
+
maxSize: number;
|
|
68
|
+
ttlSeconds: number;
|
|
69
|
+
}): Promise<PresignedUpload>;
|
|
70
|
+
}
|
|
71
|
+
/** 预签名下载的选项。 */
|
|
72
|
+
export interface PresignGetOptions {
|
|
73
|
+
ttlSeconds: number;
|
|
74
|
+
/** 下载文件名,进 `Content-Disposition`(RFC 6266,非 ASCII 用 `filename*=UTF-8''…`)。 */
|
|
75
|
+
downloadName?: string;
|
|
76
|
+
/**
|
|
77
|
+
* 浏览器打开方式:`attachment`(缺省,下载)或 `inline`(在页面里显示)。用户上传的内容只应对
|
|
78
|
+
* 不会执行脚本的类型(图片、PDF)用 `inline`。
|
|
79
|
+
*/
|
|
80
|
+
disposition?: "attachment" | "inline";
|
|
81
|
+
/** 覆盖响应的 `Content-Type`(如按核对过的类型返回,而不是存储里记的类型)。 */
|
|
82
|
+
responseContentType?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 预签名上传的请求方式:
|
|
86
|
+
* - `PUT`:请求体就是文件字节,请求头带上 `headers`;
|
|
87
|
+
* - `POST`:`multipart/form-data` 表单,先按原样放入 `fields` 的每一项,**最后**放文件字段 `file`;
|
|
88
|
+
* 请求头带上 `headers`(通常为空)。
|
|
89
|
+
*/
|
|
90
|
+
export interface PresignedUpload {
|
|
91
|
+
method: "PUT" | "POST";
|
|
92
|
+
url: string;
|
|
93
|
+
headers: Record<string, string>;
|
|
94
|
+
/** `POST` 时必有:表单字段(含策略与签名)。 */
|
|
95
|
+
fields?: Record<string, string>;
|
|
96
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object storage contract `object-store@1`: a single-select, brokered extension point (extensions
|
|
3
|
+
* may not `requires` it; they use it through `ctx.storage`). The provider implements
|
|
4
|
+
* {@link ObjectStore}; the `ctx.storage` an extension gets ({@link ExtensionStorage}) is wrapped by
|
|
5
|
+
* the kernel: every method takes a leading `orgId`, and keys get the prefix
|
|
6
|
+
* `plugins/<extension name>/<orgId>/`.
|
|
7
|
+
*/
|
|
8
|
+
/** Object storage extension point ref. */
|
|
9
|
+
export const OBJECT_STORE_EXTENSION_POINT = "object-store@1";
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type ParamSchema } from "./model-params.js";
|
|
2
|
+
/** 工具注解(与 MCP 对齐的提示,用于推导默认策略)。 */
|
|
3
|
+
export interface ToolAnnotations {
|
|
4
|
+
readOnlyHint?: boolean;
|
|
5
|
+
destructiveHint?: boolean;
|
|
6
|
+
idempotentHint?: boolean;
|
|
7
|
+
openWorldHint?: boolean;
|
|
8
|
+
requiresUserInteraction?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolSpec {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
inputSchema: Record<string, unknown>;
|
|
14
|
+
annotations?: ToolAnnotations;
|
|
15
|
+
/** 仅插件 / 内置工具可声明;缺省按注解推导。 */
|
|
16
|
+
defaultPolicy?: "allow" | "ask";
|
|
17
|
+
/**
|
|
18
|
+
* 仅连接器工具:连接器自己的不透明数据(如 OpenAPI 的方法、路径模板、参数位置、服务器地址),可 JSON 序列化。
|
|
19
|
+
* 内核把它与定义一起存进快照并计入指纹(变了即漂移、需复核),调用时经 `callTool` 的 `ctx.spec` 原样交回;
|
|
20
|
+
* 绝不发给模型。
|
|
21
|
+
*/
|
|
22
|
+
connectorData?: Record<string, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* Display only: a short human title shown on the tool's row instead of its key, e.g. the MCP
|
|
25
|
+
* `title` ("Search messages"); 1–80 characters, one line, with no control, bidi or invisible
|
|
26
|
+
* characters (Unicode Cc / Cf). Never sent to the model and not part of the definition
|
|
27
|
+
* fingerprint, so adding or changing it never triggers drift review.
|
|
28
|
+
*/
|
|
29
|
+
title?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Display only: up to 3 input property names whose values the tool's row shows, in order, e.g.
|
|
32
|
+
* `["query", "channel"]`. Each must be a property of `inputSchema.properties`. Never sent to the
|
|
33
|
+
* model and not part of the definition fingerprint.
|
|
34
|
+
*/
|
|
35
|
+
keyArgs?: string[];
|
|
36
|
+
}
|
|
37
|
+
/** Longest {@link ToolSpec.title}, in code points. */
|
|
38
|
+
export declare const TOOL_TITLE_MAX = 80;
|
|
39
|
+
/** Most {@link ToolSpec.keyArgs}. */
|
|
40
|
+
export declare const TOOL_KEY_ARGS_MAX = 3;
|
|
41
|
+
/**
|
|
42
|
+
* Checks the display-only {@link ToolSpec.title} and {@link ToolSpec.keyArgs}; returns the problems
|
|
43
|
+
* (empty = valid). The title is 1–80 code points, not blank and free of control and format
|
|
44
|
+
* characters (Unicode Cc / Cf); keyArgs are at most 3 unique strings, each a property of
|
|
45
|
+
* `inputSchema.properties` and free of Cc / Cf characters.
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateToolPresentation(spec: Pick<ToolSpec, "title" | "keyArgs" | "inputSchema">): string[];
|
|
48
|
+
export type ToolContent = {
|
|
49
|
+
type: "text";
|
|
50
|
+
text: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: "image";
|
|
53
|
+
mimeType: string;
|
|
54
|
+
data: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: "resource_link";
|
|
57
|
+
uri: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
mimeType?: string;
|
|
60
|
+
};
|
|
61
|
+
/** 工具调用的一句话说明,给界面显示(中英文各 ≤ 200 字符);不进模型上下文。 */
|
|
62
|
+
export interface ToolDisplay {
|
|
63
|
+
summary: {
|
|
64
|
+
zh: string;
|
|
65
|
+
en: string;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export interface ToolResult {
|
|
69
|
+
content: ToolContent[];
|
|
70
|
+
isError?: boolean;
|
|
71
|
+
/** 可选:对话流里显示这句说明,代替原样的输入 JSON。 */
|
|
72
|
+
display?: ToolDisplay;
|
|
73
|
+
/**
|
|
74
|
+
* Optional: an inline card under this call in the conversation: `view` is a ui contribution of
|
|
75
|
+
* the same extension with `place: "inline"`, rendered with `card: { callId, key }`. Display only
|
|
76
|
+
* (recorded on the `tool_result` event, never in the model's context); an unknown view is dropped
|
|
77
|
+
* with a warning and the result is kept.
|
|
78
|
+
*/
|
|
79
|
+
card?: {
|
|
80
|
+
view: string;
|
|
81
|
+
key?: string;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** 校验 {@link ToolDisplay},返回问题列表(空 = 合法);长度按 Unicode 码点计。 */
|
|
85
|
+
export declare function validateToolDisplay(d: unknown): string[];
|
|
86
|
+
/** Text shown to people: bilingual (`zh` / `en`) or a single string shown as is. */
|
|
87
|
+
export type LocalizedText = {
|
|
88
|
+
zh: string;
|
|
89
|
+
en: string;
|
|
90
|
+
} | string;
|
|
91
|
+
/** 交互表单:字段的 schema 只用 M2 的 ParamSchema 子集;枚举字段可设 multiSelect 与 allowOther。 */
|
|
92
|
+
export interface InputForm {
|
|
93
|
+
/** 插件可给双语;内核内置工具给英文(模型生成的问题原样展示)。 */
|
|
94
|
+
title: LocalizedText;
|
|
95
|
+
/**
|
|
96
|
+
* 可选:需要用户在浏览器里打开的外部页面(如 MCP URL 模式的 elicitation:去对方页面授权 / 付款 / 填敏感信息)。
|
|
97
|
+
* 界面把它显示为链接(完整地址可见);只接受 http(s) 绝对地址、≤ 2000 字符。
|
|
98
|
+
*/
|
|
99
|
+
url?: string;
|
|
100
|
+
fields: {
|
|
101
|
+
key: string;
|
|
102
|
+
label: LocalizedText;
|
|
103
|
+
description?: LocalizedText;
|
|
104
|
+
schema: ParamSchema;
|
|
105
|
+
multiSelect?: boolean;
|
|
106
|
+
allowOther?: boolean;
|
|
107
|
+
required?: boolean;
|
|
108
|
+
}[];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* 工具需要用户输入:内核暂停运行、渲染表单;用户提交后用同一 input 再次调用该工具,
|
|
112
|
+
* 并在 ctx.resume 中带回 state 与 answer。state ≤ 16 KB,可 JSON 序列化。
|
|
113
|
+
*/
|
|
114
|
+
export interface InputRequired {
|
|
115
|
+
kind: "input_required";
|
|
116
|
+
form: InputForm;
|
|
117
|
+
state?: unknown;
|
|
118
|
+
}
|
|
119
|
+
export type FormAnswer = Record<string, string | number | boolean | string[]>;
|
|
120
|
+
export interface ToolCallContext {
|
|
121
|
+
orgId: string;
|
|
122
|
+
accountId: string;
|
|
123
|
+
agentId: string;
|
|
124
|
+
conversationId: string;
|
|
125
|
+
callId: string;
|
|
126
|
+
signal: AbortSignal;
|
|
127
|
+
/** null = 用户取消或超时。 */
|
|
128
|
+
resume?: {
|
|
129
|
+
state: unknown;
|
|
130
|
+
answer: FormAnswer | null;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export type ToolHandler = (input: unknown, ctx: ToolCallContext) => Promise<ToolResult | InputRequired>;
|
|
134
|
+
/**
|
|
135
|
+
* 工具错误的类别:
|
|
136
|
+
* - `error`(缺省):远端可用、但这次调用失败(参数不对、业务错误、鉴权失败等),不计入连接器熔断;
|
|
137
|
+
* - `unavailable`:远端不可用(连接 / 传输错误、超时、HTTP 5xx),计入连接器熔断(连接器用它告诉内核)。
|
|
138
|
+
*/
|
|
139
|
+
export type ToolErrorKind = "error" | "unavailable";
|
|
140
|
+
/** 工具执行失败:message 给模型看,须为英文且可操作。 */
|
|
141
|
+
export declare class ToolError extends Error {
|
|
142
|
+
readonly kind: ToolErrorKind;
|
|
143
|
+
constructor(message: string, opts?: {
|
|
144
|
+
kind?: ToolErrorKind;
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** 插件工具名规则。 */
|
|
148
|
+
export declare const EXTENSION_TOOL_NAME: RegExp;
|
|
149
|
+
/** 校验插件注册的工具声明,返回问题列表(空 = 合法)。 */
|
|
150
|
+
export declare function validateToolSpec(spec: ToolSpec): string[];
|
|
151
|
+
/** 校验交互表单的声明,返回问题列表(空 = 合法)。 */
|
|
152
|
+
export declare function validateInputForm(form: InputForm): string[];
|
|
153
|
+
/** 校验用户提交的答案是否符合表单,返回问题列表(空 = 合法)。 */
|
|
154
|
+
export declare function validateAnswer(form: InputForm, answer: FormAnswer): string[];
|