@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.
Files changed (70) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +27 -2
  4. package/dist/agent-template.d.ts +47 -0
  5. package/dist/agent-template.js +123 -0
  6. package/dist/bootstrap.d.ts +67 -0
  7. package/dist/bootstrap.js +109 -0
  8. package/dist/cards.d.ts +64 -0
  9. package/dist/cards.js +217 -0
  10. package/dist/channel.d.ts +230 -0
  11. package/dist/channel.js +26 -0
  12. package/dist/connector-auth.d.ts +64 -0
  13. package/dist/connector-auth.js +96 -0
  14. package/dist/connector.d.ts +154 -0
  15. package/dist/connector.js +1 -0
  16. package/dist/ctx.d.ts +176 -0
  17. package/dist/ctx.js +1 -0
  18. package/dist/define-extension.d.ts +41 -0
  19. package/dist/define-extension.js +85 -0
  20. package/dist/directory.d.ts +29 -0
  21. package/dist/directory.js +1 -0
  22. package/dist/extension-point.d.ts +29 -0
  23. package/dist/extension-point.js +36 -0
  24. package/dist/file-preview.d.ts +64 -0
  25. package/dist/file-preview.js +72 -0
  26. package/dist/frontmatter.d.ts +12 -0
  27. package/dist/frontmatter.js +36 -0
  28. package/dist/hooks.d.ts +194 -0
  29. package/dist/hooks.js +14 -0
  30. package/dist/identity.d.ts +44 -0
  31. package/dist/identity.js +1 -0
  32. package/dist/index.d.ts +27 -0
  33. package/dist/index.js +27 -0
  34. package/dist/marketplace.d.ts +99 -0
  35. package/dist/marketplace.js +256 -0
  36. package/dist/model-params.d.ts +82 -0
  37. package/dist/model-params.js +222 -0
  38. package/dist/model.d.ts +227 -0
  39. package/dist/model.js +152 -0
  40. package/dist/permissions.d.ts +67 -0
  41. package/dist/permissions.js +271 -0
  42. package/dist/plugin-manifest.d.ts +141 -0
  43. package/dist/plugin-manifest.js +322 -0
  44. package/dist/plugin-package.d.ts +139 -0
  45. package/dist/plugin-package.js +448 -0
  46. package/dist/sandbox.d.ts +152 -0
  47. package/dist/sandbox.js +96 -0
  48. package/dist/secret.d.ts +5 -0
  49. package/dist/secret.js +8 -0
  50. package/dist/semver-range.d.ts +6 -0
  51. package/dist/semver-range.js +12 -0
  52. package/dist/skill-invocation.d.ts +83 -0
  53. package/dist/skill-invocation.js +201 -0
  54. package/dist/skill-keys.d.ts +9 -0
  55. package/dist/skill-keys.js +16 -0
  56. package/dist/skill.d.ts +77 -0
  57. package/dist/skill.js +244 -0
  58. package/dist/storage.d.ts +96 -0
  59. package/dist/storage.js +9 -0
  60. package/dist/tool.d.ts +154 -0
  61. package/dist/tool.js +218 -0
  62. package/dist/ui.d.ts +265 -0
  63. package/dist/ui.js +358 -0
  64. package/dist/url.d.ts +2 -0
  65. package/dist/url.js +12 -0
  66. package/package.json +40 -3
  67. package/schemas/bootstrap.v1.schema.json +125 -0
  68. package/schemas/connector-template.v1.schema.json +116 -0
  69. package/schemas/marketplace.v1.schema.json +270 -0
  70. package/schemas/plugin.v1.schema.json +330 -0
@@ -0,0 +1,256 @@
1
+ import { z } from "zod";
2
+ import { CATALOG_ITEM_NAME_RE, isSafePackagePath, NPM_PACKAGE_NAME_RE, SEMVER_VERSION_RE, SHA512_INTEGRITY_RE, } from "./plugin-package.js";
3
+ /**
4
+ * The marketplace manifest `marketplace.json` (package ecosystem spec §5.2): a list of plugins,
5
+ * each with one source. One entry is one version: a marketplace that offers several versions lists
6
+ * the plugin once per version, so `(name, marketplaceEntryVersion)` is unique in a manifest.
7
+ *
8
+ * Sources: a same-repo `path`, a `git` repo, an `npm` package (exact version + sha512, downloads
9
+ * are verified against it, never npm metadata) or an `archive` URL (sha512 or sha256 SRI).
10
+ *
11
+ * The committed JSON Schema (`schemas/marketplace.v1.schema.json`) is generated from
12
+ * {@link MarketplaceManifestSchema} by `pnpm gen:package-schemas` and drift-checked by
13
+ * `check:package-schemas`. Cross-field rules (unique `(name, version)`, `version` equal to the npm
14
+ * source's, `sensitive` ⊆ `keys`) are decided only by {@link parseMarketplaceManifest}. Unknown
15
+ * fields are dropped (forward compatible: a newer manifest does not make an older kernel reject it).
16
+ */
17
+ /** Manifest format version. */
18
+ export const MARKETPLACE_MANIFEST_SCHEMA_VERSION = 1;
19
+ /** Limits of the marketplace manifest. */
20
+ export const MARKETPLACE_LIMITS = {
21
+ nameChars: 100,
22
+ summaryChars: 280,
23
+ plugins: 5000,
24
+ permissionKeys: 500,
25
+ urlChars: 2000,
26
+ };
27
+ const M = MARKETPLACE_LIMITS;
28
+ const MAX_ERRORS = 50;
29
+ /** Kernel permission key format: `<category>[:<value>]`, the category lowercase letters and dots. */
30
+ const PERMISSION_KEY_RE = /^[a-z][a-z.]{0,62}(?::[^\r\n]{1,256})?$/;
31
+ const SHA256_INTEGRITY_RE = /^sha256-[A-Za-z0-9+/]{43}=$/;
32
+ const COMMIT_RE = /^[0-9a-f]{40}$/;
33
+ const HTTPS_URL_RE = /^https:\/\/\S+$/;
34
+ const HTTPS_OR_LOOPBACK_RE = /^(?:https:\/\/\S+|http:\/\/(?:127\.\d{1,3}\.\d{1,3}\.\d{1,3}|localhost|\[::1\])(?::\d{1,5})?(?:\/\S*)?)$/;
35
+ const urlString = (re) => z
36
+ .string()
37
+ .max(M.urlChars)
38
+ .regex(re, { message: "must be an https URL" })
39
+ .refine((s) => {
40
+ try {
41
+ new URL(s);
42
+ return true;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }, { message: "must be a valid URL" });
48
+ const exactVersion = (what) => z
49
+ .string()
50
+ .max(256)
51
+ .regex(SEMVER_VERSION_RE, { message: `${what} must be an exact semver version` });
52
+ /** `./<safe path>` inside the marketplace repo. */
53
+ const repoPath = z
54
+ .string()
55
+ .max(512)
56
+ .refine((p) => p.startsWith("./") && isSafePackagePath(p.slice(2)), {
57
+ message: 'path must start with "./" and stay inside the repository',
58
+ });
59
+ /** A path inside a git repo: `.` (the root) or a safe relative path with an optional `./`. */
60
+ const gitPath = z
61
+ .string()
62
+ .max(512)
63
+ .refine((p) => p === "." || isSafePackagePath(p.replace(/^\.\//, "")), {
64
+ message: "path must be . or a relative path inside the repository",
65
+ });
66
+ function buildSchema(opts) {
67
+ const url = urlString(opts.allowInsecureLoopback ? HTTPS_OR_LOOPBACK_RE : HTTPS_URL_RE);
68
+ const none = z.never().optional();
69
+ // Exactly one form: each object declares the other forms' keys as absent.
70
+ const source = z.union([
71
+ z.object({ path: repoPath, git: none, npm: none, archive: none }),
72
+ z.object({
73
+ git: z.object({
74
+ url,
75
+ ref: z.string().min(1).max(256).optional(),
76
+ commit: z
77
+ .string()
78
+ .regex(COMMIT_RE, { message: "commit must be 40 hex characters" })
79
+ .optional(),
80
+ path: gitPath.optional(),
81
+ }),
82
+ path: none,
83
+ npm: none,
84
+ archive: none,
85
+ }),
86
+ z.object({
87
+ npm: z.object({
88
+ package: z
89
+ .string()
90
+ .max(214)
91
+ .regex(NPM_PACKAGE_NAME_RE, { message: "package must be an npm package name" }),
92
+ version: exactVersion("version"),
93
+ integrity: z
94
+ .string()
95
+ .regex(SHA512_INTEGRITY_RE, { message: "integrity must be sha512-<86 base64 chars>==" }),
96
+ }),
97
+ path: none,
98
+ git: none,
99
+ archive: none,
100
+ }),
101
+ z.object({
102
+ archive: z.object({
103
+ url,
104
+ integrity: z
105
+ .string()
106
+ .refine((s) => SHA512_INTEGRITY_RE.test(s) || SHA256_INTEGRITY_RE.test(s), {
107
+ message: "integrity must be a sha512- or sha256- SRI",
108
+ }),
109
+ }),
110
+ path: none,
111
+ git: none,
112
+ npm: none,
113
+ }),
114
+ ]);
115
+ const permissionKey = z.string().max(300).regex(PERMISSION_KEY_RE, {
116
+ message: "must be a permission key (<category>[:<value>])",
117
+ });
118
+ const entry = z.object({
119
+ name: z
120
+ .string()
121
+ .regex(CATALOG_ITEM_NAME_RE, { message: "name must match ^[a-z0-9][a-z0-9-]{0,63}$" }),
122
+ summary: z.object({
123
+ en: z.string().min(1).max(M.summaryChars),
124
+ zh: z.string().min(1).max(M.summaryChars).optional(),
125
+ }),
126
+ version: exactVersion("version").optional(),
127
+ minKernel: exactVersion("minKernel").optional(),
128
+ permissions: z
129
+ .object({
130
+ keys: z.array(permissionKey).max(M.permissionKeys),
131
+ sensitive: z.array(permissionKey).max(M.permissionKeys),
132
+ })
133
+ .optional(),
134
+ homepage: urlString(HTTPS_URL_RE).optional(),
135
+ source,
136
+ });
137
+ return z
138
+ .object({
139
+ $schema: z.string().max(M.urlChars).optional(),
140
+ schemaVersion: z.literal(MARKETPLACE_MANIFEST_SCHEMA_VERSION),
141
+ name: z.string().min(1).max(M.nameChars),
142
+ owner: z
143
+ .object({
144
+ name: z.string().min(1).max(M.nameChars),
145
+ url: urlString(HTTPS_URL_RE).optional(),
146
+ })
147
+ .optional(),
148
+ description: z.string().min(1).max(1024).optional(),
149
+ plugins: z.array(entry).max(M.plugins),
150
+ })
151
+ .superRefine((m, ctx) => {
152
+ const seen = new Set();
153
+ m.plugins.forEach((raw, i) => {
154
+ const e = raw;
155
+ const v = marketplaceEntryVersion(e);
156
+ const key = `${e.name} ${v ?? "(no version)"}`;
157
+ if (seen.has(key)) {
158
+ ctx.addIssue({
159
+ code: "custom",
160
+ path: ["plugins", i, "name"],
161
+ message: `duplicate entry ${key}`,
162
+ });
163
+ }
164
+ seen.add(key);
165
+ if ("npm" in e.source &&
166
+ e.source.npm &&
167
+ e.version !== undefined &&
168
+ e.version !== e.source.npm.version) {
169
+ ctx.addIssue({
170
+ code: "custom",
171
+ path: ["plugins", i, "version"],
172
+ message: `version ${e.version} must equal source.npm.version ${e.source.npm.version}`,
173
+ });
174
+ }
175
+ if (e.permissions) {
176
+ const keys = new Set(e.permissions.keys);
177
+ for (const s of e.permissions.sensitive) {
178
+ if (!keys.has(s)) {
179
+ ctx.addIssue({
180
+ code: "custom",
181
+ path: ["plugins", i, "permissions", "sensitive"],
182
+ message: `sensitive key "${s}" is not in keys`,
183
+ });
184
+ }
185
+ }
186
+ }
187
+ });
188
+ });
189
+ }
190
+ /**
191
+ * The zod schema of `marketplace.json` (https only; non-strict: unknown fields dropped). The JSON
192
+ * Schema is generated with `z.toJSONSchema(MarketplaceManifestSchema, { io: "input" })`.
193
+ */
194
+ export const MarketplaceManifestSchema = buildSchema({});
195
+ const LOOPBACK_SCHEMA = buildSchema({
196
+ allowInsecureLoopback: true,
197
+ });
198
+ /** Strips the absent-form keys zod leaves as `undefined` on the parsed source. */
199
+ function cleanSource(s) {
200
+ return Object.fromEntries(Object.entries(s).filter(([, v]) => v !== undefined));
201
+ }
202
+ /**
203
+ * Validates and parses `marketplace.json` (unknown fields dropped). Invalid input returns
204
+ * `{ errors }` (`<path>: <reason>`, at most 50); never throws.
205
+ */
206
+ export function parseMarketplaceManifest(json, opts = {}) {
207
+ const r = (opts.allowInsecureLoopback ? LOOPBACK_SCHEMA : MarketplaceManifestSchema).safeParse(json);
208
+ if (r.success) {
209
+ return {
210
+ ...r.data,
211
+ plugins: r.data.plugins.map((e) => ({
212
+ ...e,
213
+ source: cleanSource(e.source),
214
+ })),
215
+ };
216
+ }
217
+ return {
218
+ errors: r.error.issues
219
+ .flatMap((i) => explainSource(json, i))
220
+ .slice(0, MAX_ERRORS)
221
+ .map((i) => {
222
+ const path = i.path.map(String).join(".");
223
+ return path ? `${path}: ${i.message}` : i.message;
224
+ }),
225
+ };
226
+ }
227
+ const SOURCE_FORMS = ["path", "git", "npm", "archive"];
228
+ /**
229
+ * A failed `source` union is reported by zod as a bare "Invalid input"; report the problems of the
230
+ * form the source uses instead, or that it must use exactly one form.
231
+ */
232
+ function explainSource(json, issue) {
233
+ if (issue.code !== "invalid_union" || issue.path[issue.path.length - 1] !== "source") {
234
+ return [issue];
235
+ }
236
+ let v = json;
237
+ for (const k of issue.path)
238
+ v = v?.[k];
239
+ const used = v !== null && typeof v === "object"
240
+ ? SOURCE_FORMS.filter((f) => v[f] !== undefined)
241
+ : [];
242
+ const branch = used.length === 1 ? SOURCE_FORMS.indexOf(used[0]) : -1;
243
+ const branchIssues = branch >= 0 ? issue.errors[branch] : undefined;
244
+ if (!branchIssues || branchIssues.length === 0) {
245
+ return [{ path: issue.path, message: `must have exactly one of ${SOURCE_FORMS.join(", ")}` }];
246
+ }
247
+ return branchIssues.map((b) => ({ path: [...issue.path, ...b.path], message: b.message }));
248
+ }
249
+ /** The entry's version: `version`, else the npm source's version, else `null` (the plugin's own). */
250
+ export function marketplaceEntryVersion(e) {
251
+ if (e.version !== undefined)
252
+ return e.version;
253
+ if ("npm" in e.source)
254
+ return e.source.npm.version;
255
+ return null;
256
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * `model-provider@2` 的参数声明与校验(纯函数;issue / 错误信息用英文,供日志与 API 返回)。
3
+ * 每个模型在 `ModelInfo.parameters` 中声明自己支持的参数,取值约束是受限的 JSON Schema 子集。
4
+ */
5
+ export type Effort = "low" | "medium" | "high" | "xhigh" | "max";
6
+ /**
7
+ * 标准旋钮:平台统一语义,界面专门渲染,换模型或兜底时语义可迁移;插件负责映射到原生参数。
8
+ * - `effort`:思考深度,`{ type: "string", enum }`,取值是 `low|medium|high|xhigh|max` 的子集;
9
+ * - `thinking`:思考模式,`{ type: "string", enum }`,取值是 `off|on|summarized` 的子集;
10
+ * - `temperature`:`{ type: "number" }`(可带范围);
11
+ * - `maxOutputTokens`:最大输出 token,`{ type: "integer" }`(可带范围)。
12
+ */
13
+ export type StandardKnob = "effort" | "thinking" | "temperature" | "maxOutputTokens";
14
+ export declare const STANDARD_KNOBS: readonly StandardKnob[];
15
+ /** `effort` 旋钮允许的取值全集。 */
16
+ export declare const EFFORT_VALUES: readonly Effort[];
17
+ /** `thinking` 旋钮允许的取值全集。 */
18
+ export declare const THINKING_VALUES: readonly string[];
19
+ /** 参数取值约束:受限的 JSON Schema 子集(可序列化后交给界面;内核自行校验,不依赖 ajv)。 */
20
+ export type ParamSchema = {
21
+ type: "string";
22
+ enum: string[];
23
+ } | {
24
+ type: "string";
25
+ maxLength?: number;
26
+ } | {
27
+ type: "boolean";
28
+ } | {
29
+ type: "number" | "integer";
30
+ minimum?: number;
31
+ maximum?: number;
32
+ };
33
+ export type ParamValue = string | number | boolean;
34
+ /** 模型声明的一个参数。 */
35
+ export interface ModelParameter {
36
+ /** 标准旋钮名(见 {@link StandardKnob}),或厂商专有参数 `"<插件名>.<名称>"`。 */
37
+ key: string;
38
+ /** key 为标准旋钮时必填且必须等于 key;厂商专有参数不得填写。 */
39
+ standard?: StandardKnob;
40
+ schema: ParamSchema;
41
+ /** 未设置时使用的值;必须满足 schema。 */
42
+ default?: ParamValue;
43
+ label: {
44
+ zh: string;
45
+ en: string;
46
+ };
47
+ description?: {
48
+ zh: string;
49
+ en: string;
50
+ };
51
+ }
52
+ export interface ParamIssue {
53
+ key: string;
54
+ message: string;
55
+ }
56
+ /** 值不满足 schema 时返回英文原因,满足时返回 undefined。 */
57
+ export declare function checkParamValue(schema: ParamSchema, v: unknown): string | undefined;
58
+ /**
59
+ * 严格校验(保存 agent 时用):未知键、类型不符、枚举越界、超出范围都算错,逐键报告。
60
+ * 成功时 `value` 为传入的参数,不补默认值;值为 undefined 的键视为未设置。
61
+ */
62
+ export declare function validateParams(decl: ModelParameter[], params: Record<string, unknown>): {
63
+ ok: true;
64
+ value: Record<string, ParamValue>;
65
+ } | {
66
+ ok: false;
67
+ issues: ParamIssue[];
68
+ };
69
+ /**
70
+ * 宽松版(运行时用):丢弃未知或不合法的项(键记入 `dropped`),再为未设置的已声明参数补默认值。
71
+ * 用于插件升级后已保存的参数不再合法的情况,不让整轮运行失败。
72
+ */
73
+ export declare function sanitizeParams(decl: ModelParameter[], params: Record<string, unknown>): {
74
+ value: Record<string, ParamValue>;
75
+ dropped: string[];
76
+ };
77
+ /**
78
+ * 校验插件为一个模型声明的参数:键名是标准旋钮或 `<plugin>.<名称>`、`standard` 与键一致、
79
+ * schema 属于受支持的子集、标准旋钮符合取值约定、默认值满足 schema、文案齐全、键不重复。
80
+ * 有问题时抛错,信息点名插件与各个键(英文)。
81
+ */
82
+ export declare function assertValidDeclarations(plugin: string, decl: ModelParameter[]): void;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * `model-provider@2` 的参数声明与校验(纯函数;issue / 错误信息用英文,供日志与 API 返回)。
3
+ * 每个模型在 `ModelInfo.parameters` 中声明自己支持的参数,取值约束是受限的 JSON Schema 子集。
4
+ */
5
+ export const STANDARD_KNOBS = [
6
+ "effort",
7
+ "thinking",
8
+ "temperature",
9
+ "maxOutputTokens",
10
+ ];
11
+ /** `effort` 旋钮允许的取值全集。 */
12
+ export const EFFORT_VALUES = ["low", "medium", "high", "xhigh", "max"];
13
+ /** `thinking` 旋钮允许的取值全集。 */
14
+ export const THINKING_VALUES = ["off", "on", "summarized"];
15
+ const isStandardKnob = (k) => STANDARD_KNOBS.includes(k);
16
+ const VENDOR_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
17
+ /** 值不满足 schema 时返回英文原因,满足时返回 undefined。 */
18
+ export function checkParamValue(schema, v) {
19
+ switch (schema.type) {
20
+ case "boolean":
21
+ return typeof v === "boolean" ? undefined : "must be a boolean";
22
+ case "string":
23
+ if (typeof v !== "string")
24
+ return "must be a string";
25
+ if ("enum" in schema) {
26
+ return schema.enum.includes(v)
27
+ ? undefined
28
+ : `must be one of ${schema.enum.map((x) => JSON.stringify(x)).join(", ")}`;
29
+ }
30
+ if (schema.maxLength !== undefined && v.length > schema.maxLength)
31
+ return `must be at most ${schema.maxLength} characters`;
32
+ return undefined;
33
+ case "number":
34
+ case "integer":
35
+ if (typeof v !== "number" || !Number.isFinite(v))
36
+ return "must be a finite number";
37
+ if (schema.type === "integer" && !Number.isInteger(v))
38
+ return "must be an integer";
39
+ if (schema.minimum !== undefined && v < schema.minimum)
40
+ return `must be >= ${schema.minimum}`;
41
+ if (schema.maximum !== undefined && v > schema.maximum)
42
+ return `must be <= ${schema.maximum}`;
43
+ return undefined;
44
+ default:
45
+ return `has unsupported schema type ${JSON.stringify(schema.type)}`;
46
+ }
47
+ }
48
+ /** 按键查声明(用 Map,避免 `constructor` 等原型属性被当成已声明的键)。 */
49
+ const declMap = (decl) => new Map(decl.map((d) => [d.key, d]));
50
+ /**
51
+ * 严格校验(保存 agent 时用):未知键、类型不符、枚举越界、超出范围都算错,逐键报告。
52
+ * 成功时 `value` 为传入的参数,不补默认值;值为 undefined 的键视为未设置。
53
+ */
54
+ export function validateParams(decl, params) {
55
+ const byKey = declMap(decl);
56
+ const value = {};
57
+ const issues = [];
58
+ for (const [key, v] of Object.entries(params)) {
59
+ if (v === undefined)
60
+ continue;
61
+ const d = byKey.get(key);
62
+ if (!d) {
63
+ issues.push({ key, message: `unknown parameter "${key}"` });
64
+ continue;
65
+ }
66
+ const why = checkParamValue(d.schema, v);
67
+ if (why)
68
+ issues.push({ key, message: `parameter "${key}" ${why}` });
69
+ else
70
+ value[key] = v;
71
+ }
72
+ return issues.length > 0 ? { ok: false, issues } : { ok: true, value };
73
+ }
74
+ /**
75
+ * 宽松版(运行时用):丢弃未知或不合法的项(键记入 `dropped`),再为未设置的已声明参数补默认值。
76
+ * 用于插件升级后已保存的参数不再合法的情况,不让整轮运行失败。
77
+ */
78
+ export function sanitizeParams(decl, params) {
79
+ const byKey = declMap(decl);
80
+ const value = {};
81
+ const dropped = [];
82
+ for (const [key, v] of Object.entries(params)) {
83
+ if (v === undefined)
84
+ continue;
85
+ const d = byKey.get(key);
86
+ if (!d || checkParamValue(d.schema, v) !== undefined)
87
+ dropped.push(key);
88
+ else
89
+ value[key] = v;
90
+ }
91
+ for (const d of decl) {
92
+ if (!Object.hasOwn(value, d.key) && d.default !== undefined)
93
+ value[d.key] = d.default;
94
+ }
95
+ return { value, dropped };
96
+ }
97
+ function keyProblems(plugin, d, key) {
98
+ if (isStandardKnob(key)) {
99
+ return d.standard === key ? [] : [`standard knob "${key}" must set standard: "${key}"`];
100
+ }
101
+ const out = [];
102
+ if (d.standard !== undefined) {
103
+ out.push(`standard: "${d.standard}" requires key "${d.standard}"`);
104
+ }
105
+ const dot = key.indexOf(".");
106
+ const prefix = dot < 0 ? "" : key.slice(0, dot);
107
+ const name = dot < 0 ? key : key.slice(dot + 1);
108
+ if (prefix !== plugin || !VENDOR_NAME.test(name)) {
109
+ out.push(`vendor parameter key must be "${plugin}.<name>" (or a standard knob: ${STANDARD_KNOBS.join(", ")})`);
110
+ }
111
+ return out;
112
+ }
113
+ function schemaProblems(schema) {
114
+ if (typeof schema !== "object" || schema === null)
115
+ return ["schema is required"];
116
+ const s = schema;
117
+ switch (s.type) {
118
+ case "boolean":
119
+ return [];
120
+ case "string":
121
+ if ("enum" in s) {
122
+ const e = s.enum;
123
+ if (!Array.isArray(e) || e.length === 0 || !e.every((x) => typeof x === "string"))
124
+ return ["enum must be a non-empty array of strings"];
125
+ return new Set(e).size === e.length ? [] : ["enum has duplicate values"];
126
+ }
127
+ if (s.maxLength !== undefined && !(Number.isInteger(s.maxLength) && Number(s.maxLength) >= 0))
128
+ return ["maxLength must be a non-negative integer"];
129
+ return [];
130
+ case "number":
131
+ case "integer": {
132
+ const out = [];
133
+ for (const b of ["minimum", "maximum"]) {
134
+ const v = s[b];
135
+ if (v !== undefined && !(typeof v === "number" && Number.isFinite(v)))
136
+ out.push(`${b} must be a finite number`);
137
+ }
138
+ if (typeof s.minimum === "number" && typeof s.maximum === "number" && s.minimum > s.maximum)
139
+ out.push("minimum must not exceed maximum");
140
+ return out;
141
+ }
142
+ default:
143
+ return [`unsupported schema type ${JSON.stringify(s.type)}`];
144
+ }
145
+ }
146
+ /** 标准旋钮的取值约定(schema 本身已合法时才检查)。 */
147
+ function knobProblems(key, schema) {
148
+ const values = schema.type === "string" && "enum" in schema ? schema.enum : undefined;
149
+ switch (key) {
150
+ case "effort":
151
+ return values?.every((x) => EFFORT_VALUES.includes(x))
152
+ ? []
153
+ : [`must be a string enum over a subset of ${EFFORT_VALUES.join("|")}`];
154
+ case "thinking":
155
+ return values?.every((x) => THINKING_VALUES.includes(x))
156
+ ? []
157
+ : [`must be a string enum over a subset of ${THINKING_VALUES.join("|")}`];
158
+ case "temperature":
159
+ return schema.type === "number" ? [] : ['must have schema type "number"'];
160
+ case "maxOutputTokens":
161
+ return schema.type === "integer" ? [] : ['must have schema type "integer"'];
162
+ }
163
+ }
164
+ function textProblems(d) {
165
+ const out = [];
166
+ for (const f of ["label", "description"]) {
167
+ const t = d[f];
168
+ if (t === undefined && f === "description")
169
+ continue;
170
+ const ok = typeof t === "object" &&
171
+ t !== null &&
172
+ typeof t.zh === "string" &&
173
+ typeof t.en === "string" &&
174
+ t.zh.trim() !== "" &&
175
+ t.en.trim() !== "";
176
+ if (!ok)
177
+ out.push(`${f} must have non-empty zh and en strings`);
178
+ }
179
+ return out;
180
+ }
181
+ /** 一个参数声明的全部问题;无问题返回空数组。 */
182
+ function declarationProblems(plugin, d) {
183
+ if (typeof d !== "object" || d === null)
184
+ return ["declaration must be an object"];
185
+ if (typeof d.key !== "string")
186
+ return ["key must be a string"];
187
+ const out = [...keyProblems(plugin, d, d.key), ...schemaProblems(d.schema)];
188
+ if (out.length === 0) {
189
+ if (isStandardKnob(d.key))
190
+ out.push(...knobProblems(d.key, d.schema));
191
+ if (d.default !== undefined) {
192
+ const why = checkParamValue(d.schema, d.default);
193
+ if (why)
194
+ out.push(`default ${why}`);
195
+ }
196
+ }
197
+ out.push(...textProblems(d));
198
+ return out;
199
+ }
200
+ /**
201
+ * 校验插件为一个模型声明的参数:键名是标准旋钮或 `<plugin>.<名称>`、`standard` 与键一致、
202
+ * schema 属于受支持的子集、标准旋钮符合取值约定、默认值满足 schema、文案齐全、键不重复。
203
+ * 有问题时抛错,信息点名插件与各个键(英文)。
204
+ */
205
+ export function assertValidDeclarations(plugin, decl) {
206
+ if (!Array.isArray(decl)) {
207
+ throw new Error(`model provider ${plugin}: parameters must be an array`);
208
+ }
209
+ const problems = [];
210
+ const seen = new Set();
211
+ for (const d of decl) {
212
+ const key = typeof d?.key === "string" ? d.key : "<invalid>";
213
+ if (seen.has(key))
214
+ problems.push(`"${key}": duplicate key`);
215
+ seen.add(key);
216
+ for (const p of declarationProblems(plugin, d))
217
+ problems.push(`"${key}": ${p}`);
218
+ }
219
+ if (problems.length > 0) {
220
+ throw new Error(`model provider ${plugin}: invalid parameter declarations: ${problems.join("; ")}`);
221
+ }
222
+ }