@aicloud360/dsh-cloud-disk 0.1.1-alpha.1
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/README.md +30 -0
- package/README.zh.md +30 -0
- package/lib/index.js +872 -0
- package/lib/invariant.js +20 -0
- package/lib/types/index.d.ts +56 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/types.d.ts +62 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @aicloud360/dsh-cloud-disk
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
`CloudDiskRuntime` (`ctx.cloudDisk`) defines the provider-neutral native cloud-disk seam used by the 360 AI Cloud Disk UI and Agent consumers. It owns stable node, page, user, provider, and error vocabulary; it does not expose MCP transport, tool result blocks, or external API types.
|
|
6
|
+
|
|
7
|
+
## Service API
|
|
8
|
+
|
|
9
|
+
| Member | Semantics |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `registerProvider(provider)` | Registers one backend by stable id and returns a disposer. Duplicate ids throw `CLOUD_DISK_DUPLICATE_PROVIDER`. |
|
|
12
|
+
| `getUser(signal?)` | Reads normalized user information through the selected provider. |
|
|
13
|
+
| `list(request, signal?)` | Reads one normalized remote directory page. |
|
|
14
|
+
| `search(request, signal?)` | Searches remote nodes and returns stable node ids. |
|
|
15
|
+
|
|
16
|
+
Provider selection occurs at execution time. A configured `provider` id must be registered and available. Without one, exactly one available provider is required; missing, unavailable, ambiguous, and empty registries use the stable `CloudDiskError` codes.
|
|
17
|
+
|
|
18
|
+
## Security and model experience
|
|
19
|
+
|
|
20
|
+
The service accepts a credential reference in provider configuration, never credential bytes. API key resolution, HTTP requests, authentication, signing, retries, and external response parsing belong to a Host-side provider. Consumers receive only normalized domain values. A model-facing consumer must log every directory or file id it sends to the model; this package itself contributes no prompt or model-facing tool.
|
|
21
|
+
|
|
22
|
+
#### KV Cache effect
|
|
23
|
+
|
|
24
|
+
No direct invalidation. Agent and UI consumers own prompt-prefix and context updates.
|
|
25
|
+
|
|
26
|
+
## Known Limitations and Deferred Work
|
|
27
|
+
|
|
28
|
+
- This first seam contains read-only user, listing, and search operations. Mutations, transfers, approvals, and invalidation events are added only with their owning executor and tests.
|
|
29
|
+
- The service does not provide an observation or status API; availability is observed by executing an operation and routing `CloudDiskError`.
|
|
30
|
+
- The direct 360 API provider is a separate package and is not an MCP runtime dependency.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @aicloud360/dsh-cloud-disk:云盘能力
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
`CloudDiskRuntime`(`ctx.cloudDisk`)定义 360 AI 云盘 UI 和 Agent Consumer 使用的、与提供方无关的原生云盘能力 seam。本包拥有稳定的节点、分页、用户、提供方和错误词汇,不公开 MCP transport、工具结果 block 或外部 API 类型。
|
|
6
|
+
|
|
7
|
+
## 服务 API
|
|
8
|
+
|
|
9
|
+
| 成员 | 语义 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `registerProvider(provider)` | 按稳定 id 注册一个后端并返回 disposer。id 重复时抛出 `CLOUD_DISK_DUPLICATE_PROVIDER`。 |
|
|
12
|
+
| `getUser(signal?)` | 通过选中的提供方读取规范化用户信息。 |
|
|
13
|
+
| `list(request, signal?)` | 读取一个规范化的远端目录分页。 |
|
|
14
|
+
| `search(request, signal?)` | 搜索远端节点并返回稳定节点 id。 |
|
|
15
|
+
|
|
16
|
+
提供方在每次执行时选择。配置了 `provider` 时,该 id 必须已注册且可用;未配置时必须恰好存在一个可用提供方。缺失、不可用、歧义和空注册表都使用稳定的 `CloudDiskError` code。
|
|
17
|
+
|
|
18
|
+
## 安全与模型体验
|
|
19
|
+
|
|
20
|
+
提供方配置只接受 credential reference,不接受密钥字节。API Key 解析、HTTP 请求、鉴权、签名、重试和外部响应解析属于 Host 侧 Provider。Consumer 只能接收规范化领域值。面向模型的 Consumer 必须记录发送给模型的每个目录或文件 id;本包本身不贡献提示词或面向模型的工具。
|
|
21
|
+
|
|
22
|
+
#### KV Cache 影响
|
|
23
|
+
|
|
24
|
+
不会直接导致失效。Agent 和 UI Consumer 拥有提示词前缀及上下文更新。
|
|
25
|
+
|
|
26
|
+
## 已知限制与暂缓事项
|
|
27
|
+
|
|
28
|
+
- 首个 seam 只包含用户信息、目录列表和搜索。变更、传输、审批和失效事件必须与所属执行器及测试一起加入。
|
|
29
|
+
- 服务不提供观测或状态查询 API;可用性通过执行操作并路由 `CloudDiskError` 观察。
|
|
30
|
+
- 直连 360 API Provider 属于独立包,首期运行时不依赖 MCP。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
//#region ../../../vendor/cosmokit/src/misc.ts
|
|
4
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
5
|
+
function isNullable(value) {
|
|
6
|
+
return value === null || value === void 0;
|
|
7
|
+
}
|
|
8
|
+
/** Return true for non-array object values. */
|
|
9
|
+
function isPlainObject(data) {
|
|
10
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
11
|
+
}
|
|
12
|
+
/** Filter object entries and return a new object. */
|
|
13
|
+
function filterKeys(object, filter) {
|
|
14
|
+
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
|
|
15
|
+
}
|
|
16
|
+
/** Map object values while preserving the original key set. */
|
|
17
|
+
function mapValues(object, transform) {
|
|
18
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
|
|
19
|
+
}
|
|
20
|
+
/** Pick selected keys from an object, optionally including `undefined` values. */
|
|
21
|
+
function pick(source, keys, forced) {
|
|
22
|
+
if (!keys) return { ...source };
|
|
23
|
+
const result = {};
|
|
24
|
+
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region ../../../vendor/cosmokit/src/types.ts
|
|
29
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
30
|
+
function is(type, value) {
|
|
31
|
+
if (arguments.length === 1) return (value) => is(type, value);
|
|
32
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
33
|
+
}
|
|
34
|
+
function isArrayBufferLike(value) {
|
|
35
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
36
|
+
}
|
|
37
|
+
function isArrayBufferSource(value) {
|
|
38
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
39
|
+
}
|
|
40
|
+
let Binary;
|
|
41
|
+
(function(_Binary) {
|
|
42
|
+
_Binary.is = isArrayBufferLike;
|
|
43
|
+
_Binary.isSource = isArrayBufferSource;
|
|
44
|
+
function fromSource(source) {
|
|
45
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
46
|
+
else return source;
|
|
47
|
+
}
|
|
48
|
+
_Binary.fromSource = fromSource;
|
|
49
|
+
function toBase64(source) {
|
|
50
|
+
source = fromSource(source);
|
|
51
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
52
|
+
let binary = "";
|
|
53
|
+
const bytes = new Uint8Array(source);
|
|
54
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
55
|
+
return btoa(binary);
|
|
56
|
+
}
|
|
57
|
+
_Binary.toBase64 = toBase64;
|
|
58
|
+
function fromBase64(source) {
|
|
59
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
60
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
61
|
+
}
|
|
62
|
+
_Binary.fromBase64 = fromBase64;
|
|
63
|
+
function toHex(source) {
|
|
64
|
+
source = fromSource(source);
|
|
65
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
66
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
67
|
+
}
|
|
68
|
+
_Binary.toHex = toHex;
|
|
69
|
+
function fromHex(source) {
|
|
70
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
71
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
72
|
+
const buffer = [];
|
|
73
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
74
|
+
return Uint8Array.from(buffer).buffer;
|
|
75
|
+
}
|
|
76
|
+
_Binary.fromHex = fromHex;
|
|
77
|
+
})(Binary || (Binary = {}));
|
|
78
|
+
Binary.fromBase64;
|
|
79
|
+
Binary.toBase64;
|
|
80
|
+
Binary.fromHex;
|
|
81
|
+
Binary.toHex;
|
|
82
|
+
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
|
|
83
|
+
function clone(source, refs = /* @__PURE__ */ new Map()) {
|
|
84
|
+
if (!source || typeof source !== "object") return source;
|
|
85
|
+
if (is("Date", source)) return new Date(source.valueOf());
|
|
86
|
+
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
|
|
87
|
+
if (isArrayBufferLike(source)) return source.slice(0);
|
|
88
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
89
|
+
const cached = refs.get(source);
|
|
90
|
+
if (cached) return cached;
|
|
91
|
+
if (Array.isArray(source)) {
|
|
92
|
+
const result = [];
|
|
93
|
+
refs.set(source, result);
|
|
94
|
+
source.forEach((value, index) => {
|
|
95
|
+
result[index] = Reflect.apply(clone, null, [value, refs]);
|
|
96
|
+
});
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
const result = Object.create(Object.getPrototypeOf(source));
|
|
100
|
+
refs.set(source, result);
|
|
101
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
102
|
+
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
|
|
103
|
+
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
|
|
104
|
+
Reflect.defineProperty(result, key, descriptor);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/** Deeply compare arrays, dates, regexps, buffers, and plain object fields. */
|
|
109
|
+
function deepEqual(a, b, strict) {
|
|
110
|
+
if (a === b) return true;
|
|
111
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
112
|
+
if (typeof a !== typeof b) return false;
|
|
113
|
+
if (typeof a !== "object") return false;
|
|
114
|
+
if (!a || !b) return false;
|
|
115
|
+
function check(test, then) {
|
|
116
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
117
|
+
}
|
|
118
|
+
return check(Array.isArray, (a, b) => a.length === b.length && a.every((item, index) => deepEqual(item, b[index]))) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
|
|
119
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
120
|
+
const viewA = new Uint8Array(a);
|
|
121
|
+
const viewB = new Uint8Array(b);
|
|
122
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
123
|
+
return true;
|
|
124
|
+
}) ?? Object.keys({
|
|
125
|
+
...a,
|
|
126
|
+
...b
|
|
127
|
+
}).every((key) => deepEqual(a[key], b[key], strict));
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region ../../../vendor/cosmokit/src/time.ts
|
|
131
|
+
let Time;
|
|
132
|
+
(function(_Time) {
|
|
133
|
+
_Time.millisecond = 1;
|
|
134
|
+
const second = _Time.second = 1e3;
|
|
135
|
+
const minute = _Time.minute = second * 60;
|
|
136
|
+
const hour = _Time.hour = minute * 60;
|
|
137
|
+
const day = _Time.day = hour * 24;
|
|
138
|
+
const week = _Time.week = day * 7;
|
|
139
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
140
|
+
function setTimezoneOffset(offset) {
|
|
141
|
+
timezoneOffset = offset;
|
|
142
|
+
}
|
|
143
|
+
_Time.setTimezoneOffset = setTimezoneOffset;
|
|
144
|
+
function getTimezoneOffset() {
|
|
145
|
+
return timezoneOffset;
|
|
146
|
+
}
|
|
147
|
+
_Time.getTimezoneOffset = getTimezoneOffset;
|
|
148
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
149
|
+
if (typeof date === "number") date = new Date(date);
|
|
150
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
151
|
+
return Math.floor((date.valueOf() / minute - offset) / 1440);
|
|
152
|
+
}
|
|
153
|
+
_Time.getDateNumber = getDateNumber;
|
|
154
|
+
function fromDateNumber(value, offset) {
|
|
155
|
+
const date = new Date(value * day);
|
|
156
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
157
|
+
return new Date(+date + offset * minute);
|
|
158
|
+
}
|
|
159
|
+
_Time.fromDateNumber = fromDateNumber;
|
|
160
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
161
|
+
const timeRegExp = new RegExp(`^${[
|
|
162
|
+
"w(?:eek(?:s)?)?",
|
|
163
|
+
"d(?:ay(?:s)?)?",
|
|
164
|
+
"h(?:our(?:s)?)?",
|
|
165
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
166
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
167
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
168
|
+
function parseTime(source) {
|
|
169
|
+
const capture = timeRegExp.exec(source);
|
|
170
|
+
if (!capture) return 0;
|
|
171
|
+
return (parseFloat(capture[1]) * week || 0) + (parseFloat(capture[2]) * day || 0) + (parseFloat(capture[3]) * hour || 0) + (parseFloat(capture[4]) * minute || 0) + (parseFloat(capture[5]) * second || 0);
|
|
172
|
+
}
|
|
173
|
+
_Time.parseTime = parseTime;
|
|
174
|
+
function parseDate(date) {
|
|
175
|
+
const parsed = parseTime(date);
|
|
176
|
+
if (parsed) date = Date.now() + parsed;
|
|
177
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
178
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
179
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
180
|
+
}
|
|
181
|
+
_Time.parseDate = parseDate;
|
|
182
|
+
function format(ms) {
|
|
183
|
+
const abs = Math.abs(ms);
|
|
184
|
+
if (abs >= day - hour / 2) return Math.round(ms / day) + "d";
|
|
185
|
+
else if (abs >= hour - minute / 2) return Math.round(ms / hour) + "h";
|
|
186
|
+
else if (abs >= minute - second / 2) return Math.round(ms / minute) + "m";
|
|
187
|
+
else if (abs >= second) return Math.round(ms / second) + "s";
|
|
188
|
+
return ms + "ms";
|
|
189
|
+
}
|
|
190
|
+
_Time.format = format;
|
|
191
|
+
function toDigits(source, length = 2) {
|
|
192
|
+
return source.toString().padStart(length, "0");
|
|
193
|
+
}
|
|
194
|
+
_Time.toDigits = toDigits;
|
|
195
|
+
function template(template, time = /* @__PURE__ */ new Date()) {
|
|
196
|
+
return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
197
|
+
}
|
|
198
|
+
_Time.template = template;
|
|
199
|
+
})(Time || (Time = {}));
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region ../../../vendor/schemastery/src/index.ts
|
|
202
|
+
const kSchema = Symbol.for("schemastery");
|
|
203
|
+
const kValidationError = Symbol.for("ValidationError");
|
|
204
|
+
globalThis.__schemastery_index__ ??= 0;
|
|
205
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
206
|
+
var ValidationError = class extends TypeError {
|
|
207
|
+
options;
|
|
208
|
+
name = "ValidationError";
|
|
209
|
+
constructor(message, options) {
|
|
210
|
+
let prefix = "$";
|
|
211
|
+
for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
|
|
212
|
+
else if (typeof segment === "number") prefix += "[" + segment + "]";
|
|
213
|
+
else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
|
|
214
|
+
if (prefix.startsWith(".")) prefix = prefix.slice(1);
|
|
215
|
+
super((prefix === "$" ? "" : `${prefix} `) + message);
|
|
216
|
+
this.options = options;
|
|
217
|
+
}
|
|
218
|
+
static is(error) {
|
|
219
|
+
return !!error?.[kValidationError];
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
223
|
+
const Schema = function(options) {
|
|
224
|
+
const schema = function(data, options = {}) {
|
|
225
|
+
return Schema.resolve(data, schema, options)[0];
|
|
226
|
+
};
|
|
227
|
+
if (options.refs) {
|
|
228
|
+
const refs = mapValues(options.refs, (options) => new Schema(options));
|
|
229
|
+
const getRef = (uid) => refs[uid];
|
|
230
|
+
for (const key in refs) {
|
|
231
|
+
const options = refs[key];
|
|
232
|
+
options.sKey = getRef(options.sKey);
|
|
233
|
+
options.inner = getRef(options.inner);
|
|
234
|
+
options.list = options.list && options.list.map(getRef);
|
|
235
|
+
options.dict = options.dict && mapValues(options.dict, getRef);
|
|
236
|
+
}
|
|
237
|
+
return refs[options.uid];
|
|
238
|
+
}
|
|
239
|
+
Object.assign(schema, options);
|
|
240
|
+
if (typeof schema.callback === "string") try {
|
|
241
|
+
schema.callback = new Function("return " + schema.callback)();
|
|
242
|
+
} catch {}
|
|
243
|
+
Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
|
|
244
|
+
Object.setPrototypeOf(schema, Schema.prototype);
|
|
245
|
+
schema.meta ||= {};
|
|
246
|
+
schema.toString = schema.toString.bind(schema);
|
|
247
|
+
return schema;
|
|
248
|
+
};
|
|
249
|
+
Schema.prototype = Object.create(Function.prototype);
|
|
250
|
+
Schema.prototype[kSchema] = true;
|
|
251
|
+
Object.defineProperty(Schema.prototype, "~standard", { get() {
|
|
252
|
+
return {
|
|
253
|
+
version: 1,
|
|
254
|
+
vendor: "schemastery",
|
|
255
|
+
validate: (value) => {
|
|
256
|
+
try {
|
|
257
|
+
return { value: Schema.resolve(value, this, {})[0] };
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (ValidationError.is(error)) return { issues: [{
|
|
260
|
+
message: error.message,
|
|
261
|
+
path: error.options.path
|
|
262
|
+
}] };
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
} });
|
|
268
|
+
Schema.ValidationError = ValidationError;
|
|
269
|
+
Schema.prototype.toJSON = function toJSON() {
|
|
270
|
+
if (globalThis.__schemastery_refs__) {
|
|
271
|
+
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
|
|
272
|
+
return this.uid;
|
|
273
|
+
}
|
|
274
|
+
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
|
|
275
|
+
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
|
|
276
|
+
const result = {
|
|
277
|
+
uid: this.uid,
|
|
278
|
+
refs: globalThis.__schemastery_refs__
|
|
279
|
+
};
|
|
280
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
281
|
+
return result;
|
|
282
|
+
};
|
|
283
|
+
Schema.prototype.set = function set(key, value) {
|
|
284
|
+
this.dict[key] = value;
|
|
285
|
+
return this;
|
|
286
|
+
};
|
|
287
|
+
Schema.prototype.push = function push(value) {
|
|
288
|
+
this.list.push(value);
|
|
289
|
+
return this;
|
|
290
|
+
};
|
|
291
|
+
function mergeDesc(original, messages) {
|
|
292
|
+
const result = typeof original === "string" ? { "": original } : { ...original };
|
|
293
|
+
for (const locale in messages) {
|
|
294
|
+
const value = messages[locale];
|
|
295
|
+
if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
|
|
296
|
+
else if (typeof value === "string") result[locale] = value;
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
function getInner(value) {
|
|
301
|
+
return value?.$value ?? value?.$inner;
|
|
302
|
+
}
|
|
303
|
+
function extractKeys(data) {
|
|
304
|
+
return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
|
|
305
|
+
}
|
|
306
|
+
Schema.prototype.i18n = function i18n(messages) {
|
|
307
|
+
const schema = Schema(this);
|
|
308
|
+
const desc = mergeDesc(schema.meta.description, messages);
|
|
309
|
+
if (Object.keys(desc).length) schema.meta.description = desc;
|
|
310
|
+
if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
|
|
311
|
+
return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
|
|
312
|
+
});
|
|
313
|
+
if (schema.list) schema.list = schema.list.map((inner, index) => {
|
|
314
|
+
return inner.i18n(mapValues(messages, (data = {}) => {
|
|
315
|
+
if (Array.isArray(getInner(data))) return getInner(data)[index];
|
|
316
|
+
if (Array.isArray(data)) return data[index];
|
|
317
|
+
return extractKeys(data);
|
|
318
|
+
}));
|
|
319
|
+
});
|
|
320
|
+
if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
|
|
321
|
+
if (getInner(data)) return getInner(data);
|
|
322
|
+
return extractKeys(data);
|
|
323
|
+
}));
|
|
324
|
+
if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
|
|
325
|
+
return schema;
|
|
326
|
+
};
|
|
327
|
+
Schema.prototype.extra = function extra(key, value) {
|
|
328
|
+
const schema = Schema(this);
|
|
329
|
+
schema.meta = {
|
|
330
|
+
...schema.meta,
|
|
331
|
+
[key]: value
|
|
332
|
+
};
|
|
333
|
+
return schema;
|
|
334
|
+
};
|
|
335
|
+
for (const key of [
|
|
336
|
+
"required",
|
|
337
|
+
"disabled",
|
|
338
|
+
"collapse",
|
|
339
|
+
"hidden",
|
|
340
|
+
"loose"
|
|
341
|
+
]) Object.assign(Schema.prototype, { [key](value = true) {
|
|
342
|
+
const schema = Schema(this);
|
|
343
|
+
schema.meta = {
|
|
344
|
+
...schema.meta,
|
|
345
|
+
[key]: value
|
|
346
|
+
};
|
|
347
|
+
return schema;
|
|
348
|
+
} });
|
|
349
|
+
Schema.prototype.deprecated = function deprecated() {
|
|
350
|
+
const schema = Schema(this);
|
|
351
|
+
schema.meta.badges ||= [];
|
|
352
|
+
schema.meta.badges.push({
|
|
353
|
+
text: "deprecated",
|
|
354
|
+
type: "danger"
|
|
355
|
+
});
|
|
356
|
+
return schema;
|
|
357
|
+
};
|
|
358
|
+
Schema.prototype.experimental = function experimental() {
|
|
359
|
+
const schema = Schema(this);
|
|
360
|
+
schema.meta.badges ||= [];
|
|
361
|
+
schema.meta.badges.push({
|
|
362
|
+
text: "experimental",
|
|
363
|
+
type: "warning"
|
|
364
|
+
});
|
|
365
|
+
return schema;
|
|
366
|
+
};
|
|
367
|
+
Schema.prototype.pattern = function pattern(regexp) {
|
|
368
|
+
const schema = Schema(this);
|
|
369
|
+
const pattern = pick(regexp, ["source", "flags"]);
|
|
370
|
+
schema.meta = {
|
|
371
|
+
...schema.meta,
|
|
372
|
+
pattern
|
|
373
|
+
};
|
|
374
|
+
return schema;
|
|
375
|
+
};
|
|
376
|
+
Schema.prototype.simplify = function simplify(value) {
|
|
377
|
+
if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
|
|
378
|
+
if (isNullable(value)) return value;
|
|
379
|
+
if (this.type === "object" || this.type === "dict") {
|
|
380
|
+
const result = {};
|
|
381
|
+
for (const key in value) {
|
|
382
|
+
const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
|
|
383
|
+
if (this.type === "dict" || !isNullable(item)) result[key] = item;
|
|
384
|
+
}
|
|
385
|
+
if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
|
|
386
|
+
return result;
|
|
387
|
+
} else if (this.type === "array" || this.type === "tuple") {
|
|
388
|
+
const result = [];
|
|
389
|
+
value.forEach((value, index) => {
|
|
390
|
+
const schema = this.type === "array" ? this.inner : this.list[index];
|
|
391
|
+
const item = schema ? schema.simplify(value) : value;
|
|
392
|
+
result.push(item);
|
|
393
|
+
});
|
|
394
|
+
return result;
|
|
395
|
+
} else if (this.type === "intersect") {
|
|
396
|
+
const result = {};
|
|
397
|
+
for (const item of this.list) Object.assign(result, item.simplify(value));
|
|
398
|
+
return result;
|
|
399
|
+
} else if (this.type === "union") for (const schema of this.list) try {
|
|
400
|
+
Schema.resolve(value, schema, {});
|
|
401
|
+
return schema.simplify(value);
|
|
402
|
+
} catch {}
|
|
403
|
+
return value;
|
|
404
|
+
};
|
|
405
|
+
Schema.prototype.toString = function toString(inline) {
|
|
406
|
+
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
|
|
407
|
+
};
|
|
408
|
+
Schema.prototype.role = function role(role, extra) {
|
|
409
|
+
const schema = Schema(this);
|
|
410
|
+
schema.meta = {
|
|
411
|
+
...schema.meta,
|
|
412
|
+
role,
|
|
413
|
+
extra
|
|
414
|
+
};
|
|
415
|
+
return schema;
|
|
416
|
+
};
|
|
417
|
+
for (const key of [
|
|
418
|
+
"default",
|
|
419
|
+
"link",
|
|
420
|
+
"comment",
|
|
421
|
+
"description",
|
|
422
|
+
"max",
|
|
423
|
+
"min",
|
|
424
|
+
"step"
|
|
425
|
+
]) Object.assign(Schema.prototype, { [key](value) {
|
|
426
|
+
const schema = Schema(this);
|
|
427
|
+
schema.meta = {
|
|
428
|
+
...schema.meta,
|
|
429
|
+
[key]: value
|
|
430
|
+
};
|
|
431
|
+
return schema;
|
|
432
|
+
} });
|
|
433
|
+
const resolvers = {};
|
|
434
|
+
Schema.extend = function extend(type, resolve) {
|
|
435
|
+
resolvers[type] = resolve;
|
|
436
|
+
};
|
|
437
|
+
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
|
|
438
|
+
if (!schema) return [data];
|
|
439
|
+
if (options.ignore?.(data, schema)) return [data];
|
|
440
|
+
if (isNullable(data) && schema.type !== "lazy") {
|
|
441
|
+
if (schema.meta.required) throw new ValidationError(`missing required value`, options);
|
|
442
|
+
let current = schema;
|
|
443
|
+
let fallback = schema.meta.default;
|
|
444
|
+
while (current?.type === "intersect" && isNullable(fallback)) {
|
|
445
|
+
current = current.list[0];
|
|
446
|
+
fallback = current?.meta.default;
|
|
447
|
+
}
|
|
448
|
+
if (isNullable(fallback)) return [data];
|
|
449
|
+
data = clone(fallback);
|
|
450
|
+
}
|
|
451
|
+
const callback = resolvers[schema.type];
|
|
452
|
+
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
|
|
453
|
+
try {
|
|
454
|
+
return callback(data, schema, options, strict);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (!schema.meta.loose) throw error;
|
|
457
|
+
return [schema.meta.default];
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
Schema.from = function from(source) {
|
|
461
|
+
if (isNullable(source)) return Schema.any();
|
|
462
|
+
else if ([
|
|
463
|
+
"string",
|
|
464
|
+
"number",
|
|
465
|
+
"boolean"
|
|
466
|
+
].includes(typeof source)) return Schema.const(source).required();
|
|
467
|
+
else if (source[kSchema]) return source;
|
|
468
|
+
else if (typeof source === "function") switch (source) {
|
|
469
|
+
case String: return Schema.string().required();
|
|
470
|
+
case Number: return Schema.number().required();
|
|
471
|
+
case Boolean: return Schema.boolean().required();
|
|
472
|
+
case Function: return Schema.function().required();
|
|
473
|
+
default: return Schema.is(source).required();
|
|
474
|
+
}
|
|
475
|
+
else throw new TypeError(`cannot infer schema from ${source}`);
|
|
476
|
+
};
|
|
477
|
+
Schema.lazy = function lazy(builder) {
|
|
478
|
+
const toJSON = () => {
|
|
479
|
+
if (!schema.inner[kSchema]) {
|
|
480
|
+
schema.inner = schema.builder();
|
|
481
|
+
schema.inner.meta = {
|
|
482
|
+
...schema.meta,
|
|
483
|
+
...schema.inner.meta
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
return schema.inner.toJSON();
|
|
487
|
+
};
|
|
488
|
+
const schema = new Schema({
|
|
489
|
+
type: "lazy",
|
|
490
|
+
builder,
|
|
491
|
+
inner: { toJSON }
|
|
492
|
+
});
|
|
493
|
+
return schema;
|
|
494
|
+
};
|
|
495
|
+
Schema.natural = function natural() {
|
|
496
|
+
return Schema.number().step(1).min(0);
|
|
497
|
+
};
|
|
498
|
+
Schema.percent = function percent() {
|
|
499
|
+
return Schema.number().step(.01).min(0).max(1).role("slider");
|
|
500
|
+
};
|
|
501
|
+
Schema.date = function date() {
|
|
502
|
+
return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
|
|
503
|
+
const date = new Date(value);
|
|
504
|
+
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
|
|
505
|
+
return date;
|
|
506
|
+
}, true)]);
|
|
507
|
+
};
|
|
508
|
+
Schema.regExp = function regExp(flag = "") {
|
|
509
|
+
return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
|
|
510
|
+
try {
|
|
511
|
+
return new RegExp(value, flag);
|
|
512
|
+
} catch (e) {
|
|
513
|
+
throw new ValidationError(e.message, options);
|
|
514
|
+
}
|
|
515
|
+
}, true)]);
|
|
516
|
+
};
|
|
517
|
+
Schema.arrayBuffer = function arrayBuffer(encoding) {
|
|
518
|
+
return Schema.union([
|
|
519
|
+
Schema.is(ArrayBuffer),
|
|
520
|
+
Schema.is(SharedArrayBuffer),
|
|
521
|
+
Schema.transform(Schema.any(), (value, options) => {
|
|
522
|
+
if (Binary.isSource(value)) return Binary.fromSource(value);
|
|
523
|
+
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
|
|
524
|
+
}, true),
|
|
525
|
+
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
|
526
|
+
try {
|
|
527
|
+
return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
throw new ValidationError(e.message, options);
|
|
530
|
+
}
|
|
531
|
+
}, true)] : []
|
|
532
|
+
]);
|
|
533
|
+
};
|
|
534
|
+
Schema.extend("lazy", (data, schema, options, strict) => {
|
|
535
|
+
if (!schema.inner[kSchema]) {
|
|
536
|
+
schema.inner = schema.builder();
|
|
537
|
+
schema.inner.meta = {
|
|
538
|
+
...schema.meta,
|
|
539
|
+
...schema.inner.meta
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return Schema.resolve(data, schema.inner, options, strict);
|
|
543
|
+
});
|
|
544
|
+
Schema.extend("any", (data) => {
|
|
545
|
+
return [data];
|
|
546
|
+
});
|
|
547
|
+
Schema.extend("never", (data, _, options) => {
|
|
548
|
+
throw new ValidationError(`expected nullable but got ${data}`, options);
|
|
549
|
+
});
|
|
550
|
+
Schema.extend("const", (data, { value }, options) => {
|
|
551
|
+
if (deepEqual(data, value)) return [value];
|
|
552
|
+
throw new ValidationError(`expected ${value} but got ${data}`, options);
|
|
553
|
+
});
|
|
554
|
+
function checkWithinRange(data, meta, description, options, skipMin = false) {
|
|
555
|
+
const { max = Infinity, min = -Infinity } = meta;
|
|
556
|
+
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
|
|
557
|
+
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
|
|
558
|
+
}
|
|
559
|
+
Schema.extend("string", (data, { meta }, options) => {
|
|
560
|
+
if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
|
|
561
|
+
if (meta.pattern) {
|
|
562
|
+
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
|
|
563
|
+
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
|
|
564
|
+
}
|
|
565
|
+
checkWithinRange(data.length, meta, "string length", options);
|
|
566
|
+
return [data];
|
|
567
|
+
});
|
|
568
|
+
function decimalShift(data, digits) {
|
|
569
|
+
const str = data.toString();
|
|
570
|
+
if (str.includes("e")) return data * Math.pow(10, digits);
|
|
571
|
+
const index = str.indexOf(".");
|
|
572
|
+
if (index === -1) return data * Math.pow(10, digits);
|
|
573
|
+
const frac = str.slice(index + 1);
|
|
574
|
+
const integer = str.slice(0, index);
|
|
575
|
+
if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
|
|
576
|
+
return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
|
|
577
|
+
}
|
|
578
|
+
function isMultipleOf(data, min, step) {
|
|
579
|
+
step = Math.abs(step);
|
|
580
|
+
if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
|
|
581
|
+
const index = step.toString().indexOf(".");
|
|
582
|
+
const digits = step.toString().slice(index + 1).length;
|
|
583
|
+
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
|
|
584
|
+
}
|
|
585
|
+
Schema.extend("number", (data, { meta }, options) => {
|
|
586
|
+
if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
|
|
587
|
+
checkWithinRange(data, meta, "number", options);
|
|
588
|
+
const { step } = meta;
|
|
589
|
+
if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
|
|
590
|
+
return [data];
|
|
591
|
+
});
|
|
592
|
+
Schema.extend("boolean", (data, _, options) => {
|
|
593
|
+
if (typeof data === "boolean") return [data];
|
|
594
|
+
throw new ValidationError(`expected boolean but got ${data}`, options);
|
|
595
|
+
});
|
|
596
|
+
Schema.extend("bitset", (data, { bits, meta }, options) => {
|
|
597
|
+
let value = 0, keys = [];
|
|
598
|
+
if (typeof data === "number") {
|
|
599
|
+
value = data;
|
|
600
|
+
for (const key in bits) if (data & bits[key]) keys.push(key);
|
|
601
|
+
} else if (Array.isArray(data)) {
|
|
602
|
+
keys = data;
|
|
603
|
+
for (const key of keys) {
|
|
604
|
+
if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
|
|
605
|
+
if (key in bits) value |= bits[key];
|
|
606
|
+
}
|
|
607
|
+
} else throw new ValidationError(`expected number or array but got ${data}`, options);
|
|
608
|
+
if (value === meta.default) return [value];
|
|
609
|
+
return [value, keys];
|
|
610
|
+
});
|
|
611
|
+
Schema.extend("function", (data, _, options) => {
|
|
612
|
+
if (typeof data === "function") return [data];
|
|
613
|
+
throw new ValidationError(`expected function but got ${data}`, options);
|
|
614
|
+
});
|
|
615
|
+
Schema.extend("is", (data, { constructor }, options) => {
|
|
616
|
+
if (typeof constructor === "function") {
|
|
617
|
+
if (data instanceof constructor) return [data];
|
|
618
|
+
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
|
|
619
|
+
} else {
|
|
620
|
+
if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
621
|
+
let prototype = Object.getPrototypeOf(data);
|
|
622
|
+
while (prototype) {
|
|
623
|
+
if (prototype.constructor?.name === constructor) return [data];
|
|
624
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
625
|
+
}
|
|
626
|
+
throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
function property(data, key, schema, options) {
|
|
630
|
+
try {
|
|
631
|
+
const [value, adapted] = Schema.resolve(data[key], schema, {
|
|
632
|
+
...options,
|
|
633
|
+
path: [...options.path || [], key]
|
|
634
|
+
});
|
|
635
|
+
if (adapted !== void 0) data[key] = adapted;
|
|
636
|
+
return value;
|
|
637
|
+
} catch (e) {
|
|
638
|
+
if (!options?.autofix) throw e;
|
|
639
|
+
delete data[key];
|
|
640
|
+
return schema.meta.default;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
Schema.extend("array", (data, { inner, meta }, options) => {
|
|
644
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
645
|
+
checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
|
|
646
|
+
return [data.map((_, index) => property(data, index, inner, options))];
|
|
647
|
+
});
|
|
648
|
+
Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
|
|
649
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
650
|
+
const result = {};
|
|
651
|
+
for (const key in data) {
|
|
652
|
+
let rKey;
|
|
653
|
+
try {
|
|
654
|
+
rKey = Schema.resolve(key, sKey, options)[0];
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (strict) continue;
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
result[rKey] = property(data, key, inner, options);
|
|
660
|
+
data[rKey] = data[key];
|
|
661
|
+
if (key !== rKey) delete data[key];
|
|
662
|
+
}
|
|
663
|
+
return [result];
|
|
664
|
+
});
|
|
665
|
+
Schema.extend("tuple", (data, { list }, options, strict) => {
|
|
666
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
667
|
+
const result = list.map((inner, index) => property(data, index, inner, options));
|
|
668
|
+
if (strict) return [result];
|
|
669
|
+
result.push(...data.slice(list.length));
|
|
670
|
+
return [result];
|
|
671
|
+
});
|
|
672
|
+
function merge(result, data) {
|
|
673
|
+
for (const key in data) {
|
|
674
|
+
if (key in result) continue;
|
|
675
|
+
result[key] = data[key];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
Schema.extend("object", (data, { dict }, options, strict) => {
|
|
679
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
680
|
+
const result = {};
|
|
681
|
+
for (const key in dict) {
|
|
682
|
+
const value = property(data, key, dict[key], options);
|
|
683
|
+
if (!isNullable(value) || key in data) result[key] = value;
|
|
684
|
+
}
|
|
685
|
+
if (!strict) merge(result, data);
|
|
686
|
+
return [result];
|
|
687
|
+
});
|
|
688
|
+
Schema.extend("union", (data, { list, toString }, options, strict) => {
|
|
689
|
+
const messages = [];
|
|
690
|
+
for (const inner of list) try {
|
|
691
|
+
return Schema.resolve(data, inner, options, strict);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
messages.push(error);
|
|
694
|
+
}
|
|
695
|
+
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
696
|
+
});
|
|
697
|
+
Schema.extend("intersect", (data, { list, toString }, options, strict) => {
|
|
698
|
+
if (!list.length) return [data];
|
|
699
|
+
let result;
|
|
700
|
+
for (const inner of list) {
|
|
701
|
+
const value = Schema.resolve(data, inner, options, true)[0];
|
|
702
|
+
if (isNullable(value)) continue;
|
|
703
|
+
if (isNullable(result)) result = value;
|
|
704
|
+
else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
705
|
+
else if (typeof value === "object") merge(result ??= {}, value);
|
|
706
|
+
else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
707
|
+
}
|
|
708
|
+
if (!strict && isPlainObject(data)) merge(result, data);
|
|
709
|
+
return [result];
|
|
710
|
+
});
|
|
711
|
+
Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
|
|
712
|
+
const [result, adapted = data] = Schema.resolve(data, inner, options, true);
|
|
713
|
+
if (preserve) return [callback(result)];
|
|
714
|
+
else return [callback(result), callback(adapted)];
|
|
715
|
+
});
|
|
716
|
+
const formatters = {};
|
|
717
|
+
function defineMethod(name, keys, format) {
|
|
718
|
+
formatters[name] = format;
|
|
719
|
+
Object.assign(Schema, { [name](...args) {
|
|
720
|
+
const schema = new Schema({ type: name });
|
|
721
|
+
keys.forEach((key, index) => {
|
|
722
|
+
switch (key) {
|
|
723
|
+
case "sKey":
|
|
724
|
+
schema.sKey = args[index] ?? Schema.string();
|
|
725
|
+
break;
|
|
726
|
+
case "inner":
|
|
727
|
+
schema.inner = Schema.from(args[index]);
|
|
728
|
+
break;
|
|
729
|
+
case "list":
|
|
730
|
+
schema.list = args[index].map(Schema.from);
|
|
731
|
+
break;
|
|
732
|
+
case "dict":
|
|
733
|
+
schema.dict = mapValues(args[index], Schema.from);
|
|
734
|
+
break;
|
|
735
|
+
case "bits":
|
|
736
|
+
schema.bits = {};
|
|
737
|
+
for (const key in args[index]) {
|
|
738
|
+
if (typeof args[index][key] !== "number") continue;
|
|
739
|
+
schema.bits[key] = args[index][key];
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
case "callback": {
|
|
743
|
+
const callback = schema.callback = args[index];
|
|
744
|
+
callback["toJSON"] ||= () => callback.toString();
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "constructor": {
|
|
748
|
+
const constructor = schema.constructor = args[index];
|
|
749
|
+
if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
default: schema[key] = args[index];
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
if (name === "object" || name === "dict") schema.meta.default = {};
|
|
756
|
+
else if (name === "array" || name === "tuple") schema.meta.default = [];
|
|
757
|
+
else if (name === "bitset") schema.meta.default = 0;
|
|
758
|
+
return schema;
|
|
759
|
+
} });
|
|
760
|
+
}
|
|
761
|
+
defineMethod("is", ["constructor"], ({ constructor }) => {
|
|
762
|
+
if (typeof constructor === "function") return constructor.name;
|
|
763
|
+
else return constructor;
|
|
764
|
+
});
|
|
765
|
+
defineMethod("any", [], () => "any");
|
|
766
|
+
defineMethod("never", [], () => "never");
|
|
767
|
+
defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
|
|
768
|
+
defineMethod("string", [], () => "string");
|
|
769
|
+
defineMethod("number", [], () => "number");
|
|
770
|
+
defineMethod("boolean", [], () => "boolean");
|
|
771
|
+
defineMethod("bitset", ["bits"], () => "bitset");
|
|
772
|
+
defineMethod("function", [], () => "function");
|
|
773
|
+
defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
|
|
774
|
+
defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
|
|
775
|
+
defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
|
|
776
|
+
defineMethod("object", ["dict"], ({ dict }) => {
|
|
777
|
+
if (Object.keys(dict).length === 0) return "{}";
|
|
778
|
+
return `{ ${Object.entries(dict).map(([key, inner]) => {
|
|
779
|
+
return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
|
|
780
|
+
}).join(", ")} }`;
|
|
781
|
+
});
|
|
782
|
+
defineMethod("union", ["list"], ({ list }, inline) => {
|
|
783
|
+
const result = list.map(({ toString: format }) => format()).join(" | ");
|
|
784
|
+
return inline ? `(${result})` : result;
|
|
785
|
+
});
|
|
786
|
+
defineMethod("intersect", ["list"], ({ list }) => {
|
|
787
|
+
return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
|
|
788
|
+
});
|
|
789
|
+
defineMethod("transform", [
|
|
790
|
+
"inner",
|
|
791
|
+
"callback",
|
|
792
|
+
"preserve"
|
|
793
|
+
], ({ inner }, isInner) => inner.toString(isInner));
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region lib/types/types.js
|
|
796
|
+
/**
|
|
797
|
+
* Stable vocabulary for the CloudDisk capability seam.
|
|
798
|
+
* @module @aicloud360/dsh-cloud-disk/types
|
|
799
|
+
*/
|
|
800
|
+
/** Typed failure that consumers can route without matching external API text. */
|
|
801
|
+
var CloudDiskError = class extends HarnessError {};
|
|
802
|
+
//#endregion
|
|
803
|
+
//#region lib/types/index.js
|
|
804
|
+
/**
|
|
805
|
+
* Service Definition for the CloudDisk capability seam (`ctx.cloudDisk`).
|
|
806
|
+
* Providers expose normalized cloud-disk operations; consumers do not depend on
|
|
807
|
+
* MCP transport, external API types, or provider-specific response blocks.
|
|
808
|
+
* @module @aicloud360/dsh-cloud-disk
|
|
809
|
+
*/
|
|
810
|
+
/** Provider registry and execution service for normalized CloudDisk operations. */
|
|
811
|
+
var CloudDiskRuntime = class extends Service {
|
|
812
|
+
static Config = Schema.object({ provider: Schema.string() });
|
|
813
|
+
providers = /* @__PURE__ */ new Map();
|
|
814
|
+
providerId;
|
|
815
|
+
constructor(ctx, config) {
|
|
816
|
+
super(ctx, "cloudDisk");
|
|
817
|
+
this.providerId = config.provider;
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Register one provider and return its lifecycle disposer.
|
|
821
|
+
* @param provider - normalized provider selected by this service.
|
|
822
|
+
* @returns a disposer that withdraws the provider.
|
|
823
|
+
*/
|
|
824
|
+
registerProvider(provider) {
|
|
825
|
+
if (this.providers.has(provider.id)) throw new CloudDiskError(`provider "${provider.id}" is already registered`, "CLOUD_DISK_DUPLICATE_PROVIDER");
|
|
826
|
+
const dispose = this.ctx.effect(function* () {
|
|
827
|
+
this.providers.set(provider.id, provider);
|
|
828
|
+
yield () => this.providers.delete(provider.id);
|
|
829
|
+
}.bind(this), "cloudDisk.registerProvider()");
|
|
830
|
+
return () => void dispose();
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Read current-user information through the selected provider.
|
|
834
|
+
* @param signal - cancellation signal for the provider request.
|
|
835
|
+
* @returns normalized current-user information.
|
|
836
|
+
*/
|
|
837
|
+
async getUser(signal) {
|
|
838
|
+
return await this.selectProvider().getUser(signal);
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* List one normalized remote directory page.
|
|
842
|
+
* @param request - directory, cursor, and optional page-limit request.
|
|
843
|
+
* @param signal - cancellation signal for the provider request.
|
|
844
|
+
* @returns one page of normalized remote nodes.
|
|
845
|
+
*/
|
|
846
|
+
async list(request, signal) {
|
|
847
|
+
return await this.selectProvider().list(request, signal);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Search remote nodes using stable ids in the returned page.
|
|
851
|
+
* @param request - query, cursor, and optional page-limit request.
|
|
852
|
+
* @param signal - cancellation signal for the provider request.
|
|
853
|
+
* @returns one page of normalized matching nodes.
|
|
854
|
+
*/
|
|
855
|
+
async search(request, signal) {
|
|
856
|
+
return await this.selectProvider().search(request, signal);
|
|
857
|
+
}
|
|
858
|
+
selectProvider() {
|
|
859
|
+
if (this.providerId !== void 0) {
|
|
860
|
+
const provider = this.providers.get(this.providerId);
|
|
861
|
+
if (provider === void 0) throw new CloudDiskError(`configured provider "${this.providerId}" is missing`, "CLOUD_DISK_PROVIDER_CONFIGURED_MISSING");
|
|
862
|
+
if (!provider.available()) throw new CloudDiskError(`configured provider "${this.providerId}" is unavailable`, "CLOUD_DISK_PROVIDER_CONFIGURED_UNAVAILABLE");
|
|
863
|
+
return provider;
|
|
864
|
+
}
|
|
865
|
+
const available = [...this.providers.values()].filter((provider) => provider.available());
|
|
866
|
+
if (available.length === 0) throw new CloudDiskError("no usable CloudDisk provider is registered", "CLOUD_DISK_PROVIDER_UNAVAILABLE");
|
|
867
|
+
if (available.length > 1) throw new CloudDiskError(`multiple usable CloudDisk providers: ${available.map((provider) => provider.id).join(", ")}`, "CLOUD_DISK_PROVIDER_AMBIGUOUS");
|
|
868
|
+
return available[0];
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
//#endregion
|
|
872
|
+
export { CloudDiskError, CloudDiskRuntime, CloudDiskRuntime as default };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@aicloud360/dsh-cloud-disk`.
|
|
4
|
+
* @module @aicloud360/dsh-cloud-disk/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@aicloud360/dsh-cloud-disk";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "cloud-disk-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/** No runtime invariant: provider registrations are private and selection is enforced at execution. */
|
|
12
|
+
const install = () => {};
|
|
13
|
+
/**
|
|
14
|
+
* Register this package's invariant companion.
|
|
15
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
16
|
+
* @returns the installed registration's disposer.
|
|
17
|
+
*/
|
|
18
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
19
|
+
//#endregion
|
|
20
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Definition for the CloudDisk capability seam (`ctx.cloudDisk`).
|
|
3
|
+
* Providers expose normalized cloud-disk operations; consumers do not depend on
|
|
4
|
+
* MCP transport, external API types, or provider-specific response blocks.
|
|
5
|
+
* @module @aicloud360/dsh-cloud-disk
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
8
|
+
import z from '@deepseek-ai/schemastery';
|
|
9
|
+
import { type CloudDiskListRequest, type CloudDiskPage, type CloudDiskProvider, type CloudDiskSearchRequest, type CloudDiskUser } from './types.ts';
|
|
10
|
+
export * from './types.ts';
|
|
11
|
+
/** Runtime configuration for provider selection. */
|
|
12
|
+
export interface CloudDiskConfig {
|
|
13
|
+
/** Explicit provider id. Omitted only when exactly one usable provider exists. */
|
|
14
|
+
readonly provider?: string;
|
|
15
|
+
}
|
|
16
|
+
declare module '@deepseek-ai/cordis' {
|
|
17
|
+
interface Context {
|
|
18
|
+
cloudDisk: CloudDiskRuntime;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** Provider registry and execution service for normalized CloudDisk operations. */
|
|
22
|
+
export declare class CloudDiskRuntime extends Service {
|
|
23
|
+
static Config: z<CloudDiskConfig>;
|
|
24
|
+
private readonly providers;
|
|
25
|
+
private readonly providerId;
|
|
26
|
+
constructor(ctx: Context, config: CloudDiskConfig);
|
|
27
|
+
/**
|
|
28
|
+
* Register one provider and return its lifecycle disposer.
|
|
29
|
+
* @param provider - normalized provider selected by this service.
|
|
30
|
+
* @returns a disposer that withdraws the provider.
|
|
31
|
+
*/
|
|
32
|
+
registerProvider(provider: CloudDiskProvider): () => void;
|
|
33
|
+
/**
|
|
34
|
+
* Read current-user information through the selected provider.
|
|
35
|
+
* @param signal - cancellation signal for the provider request.
|
|
36
|
+
* @returns normalized current-user information.
|
|
37
|
+
*/
|
|
38
|
+
getUser(signal?: AbortSignal): Promise<CloudDiskUser>;
|
|
39
|
+
/**
|
|
40
|
+
* List one normalized remote directory page.
|
|
41
|
+
* @param request - directory, cursor, and optional page-limit request.
|
|
42
|
+
* @param signal - cancellation signal for the provider request.
|
|
43
|
+
* @returns one page of normalized remote nodes.
|
|
44
|
+
*/
|
|
45
|
+
list(request: CloudDiskListRequest, signal?: AbortSignal): Promise<CloudDiskPage>;
|
|
46
|
+
/**
|
|
47
|
+
* Search remote nodes using stable ids in the returned page.
|
|
48
|
+
* @param request - query, cursor, and optional page-limit request.
|
|
49
|
+
* @param signal - cancellation signal for the provider request.
|
|
50
|
+
* @returns one page of normalized matching nodes.
|
|
51
|
+
*/
|
|
52
|
+
search(request: CloudDiskSearchRequest, signal?: AbortSignal): Promise<CloudDiskPage>;
|
|
53
|
+
private selectProvider;
|
|
54
|
+
}
|
|
55
|
+
export default CloudDiskRuntime;
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@aicloud360/dsh-cloud-disk`.
|
|
3
|
+
* @module @aicloud360/dsh-cloud-disk/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "cloud-disk-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable vocabulary for the CloudDisk capability seam.
|
|
3
|
+
* @module @aicloud360/dsh-cloud-disk/types
|
|
4
|
+
*/
|
|
5
|
+
import { HarnessError } from '@deepseek-ai/dsh-llm';
|
|
6
|
+
import type { CredentialRef } from '@deepseek-ai/dsh-credentials';
|
|
7
|
+
/** Opaque identity of one remote cloud-disk node. */
|
|
8
|
+
export type CloudDiskNodeId = string & {
|
|
9
|
+
readonly __cloudDiskNodeId: unique symbol;
|
|
10
|
+
};
|
|
11
|
+
/** Remote node kind exposed to UI and Agent consumers. */
|
|
12
|
+
export type CloudDiskNodeKind = 'file' | 'directory';
|
|
13
|
+
/** One normalized remote file or directory. */
|
|
14
|
+
export interface CloudDiskNode {
|
|
15
|
+
readonly id: CloudDiskNodeId;
|
|
16
|
+
readonly kind: CloudDiskNodeKind;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly parentId?: CloudDiskNodeId;
|
|
19
|
+
readonly size?: number;
|
|
20
|
+
readonly updatedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A bounded page of remote nodes. */
|
|
23
|
+
export interface CloudDiskPage {
|
|
24
|
+
readonly nodes: readonly CloudDiskNode[];
|
|
25
|
+
readonly nextCursor?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Current-user information without credential material. */
|
|
28
|
+
export interface CloudDiskUser {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly displayName?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Request for one directory page. */
|
|
33
|
+
export interface CloudDiskListRequest {
|
|
34
|
+
readonly parentId?: CloudDiskNodeId;
|
|
35
|
+
readonly cursor?: string;
|
|
36
|
+
readonly limit?: number;
|
|
37
|
+
}
|
|
38
|
+
/** Request for a remote name/content search. */
|
|
39
|
+
export interface CloudDiskSearchRequest {
|
|
40
|
+
readonly query: string;
|
|
41
|
+
readonly cursor?: string;
|
|
42
|
+
readonly limit?: number;
|
|
43
|
+
}
|
|
44
|
+
/** Provider-neutral read capability. */
|
|
45
|
+
export interface CloudDiskProvider {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
available(): boolean;
|
|
48
|
+
getUser(signal?: AbortSignal): Promise<CloudDiskUser>;
|
|
49
|
+
list(request: CloudDiskListRequest, signal?: AbortSignal): Promise<CloudDiskPage>;
|
|
50
|
+
search(request: CloudDiskSearchRequest, signal?: AbortSignal): Promise<CloudDiskPage>;
|
|
51
|
+
}
|
|
52
|
+
/** Stable service failure codes used by all CloudDisk consumers. */
|
|
53
|
+
export type CloudDiskErrorCode = 'CLOUD_DISK_DUPLICATE_PROVIDER' | 'CLOUD_DISK_PROVIDER_CONFIGURED_MISSING' | 'CLOUD_DISK_PROVIDER_CONFIGURED_UNAVAILABLE' | 'CLOUD_DISK_PROVIDER_UNAVAILABLE' | 'CLOUD_DISK_PROVIDER_AMBIGUOUS' | 'CLOUD_DISK_CREDENTIAL_MISSING' | 'CLOUD_DISK_SIGNING_SECRET_MISSING' | 'CLOUD_DISK_AUTHENTICATION_FAILED' | 'CLOUD_DISK_NETWORK_FAILED' | 'CLOUD_DISK_INVALID_REQUEST' | 'CLOUD_DISK_PROVIDER_FAILED';
|
|
54
|
+
/** Typed failure that consumers can route without matching external API text. */
|
|
55
|
+
export declare class CloudDiskError extends HarnessError {
|
|
56
|
+
}
|
|
57
|
+
/** Host-side provider configuration; the reference is never the secret value. */
|
|
58
|
+
export interface CloudDiskProviderConfig {
|
|
59
|
+
readonly credentialRef: CredentialRef;
|
|
60
|
+
readonly endpoint: string;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aicloud360/dsh-cloud-disk",
|
|
3
|
+
"description": "Provider-neutral CloudDisk capability seam with stable node, page, user, and error vocabulary",
|
|
4
|
+
"version": "0.1.1-alpha.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public",
|
|
7
|
+
"registry": "https://registry.npmjs.org"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/yifangyun/dsh-cloud-disk.git",
|
|
12
|
+
"directory": "packages/dsh-cloud-disk"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"types": "lib/types/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"lib/index.js",
|
|
19
|
+
"lib/invariant.js",
|
|
20
|
+
"lib/types/**/*.d.ts"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@deepseek-ai/dsh-invariants": "0.1.1-rc.2",
|
|
25
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
26
|
+
"@deepseek-ai/dsh-credentials": "0.1.1-rc.2",
|
|
27
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./lib/types/index.d.ts",
|
|
32
|
+
"default": "./lib/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./invariant": {
|
|
35
|
+
"types": "./lib/types/invariant.d.ts",
|
|
36
|
+
"default": "./lib/invariant.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
}
|
|
40
|
+
}
|