@mbws/plugin-sdk 0.1.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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @mbws/plugin-sdk
2
+
3
+ Moye 插件开发 SDK——插件 panel/viewer 与宿主之间的类型定义与运行时端口适配。
4
+
5
+ ```bash
6
+ pnpm add @mbws/plugin-sdk
7
+ ```
8
+
9
+ ## 能力
10
+
11
+ - **类型**:`Manifest`(配置与权限声明)、端口调用契约等——`mbws.config.ts` 与面板代码
12
+ 共用同一套类型源
13
+ - **运行时**:宿主端口注入适配(桌面/网页宿主零感知),dev 环境自动降级 mock
14
+ (`data.read` 读 `samples/*.json`,本地 `pnpm dev` 即可跑通面板)
15
+ - **校验**:`validateManifest`(构建期与装载期单一权威校验源)
16
+
17
+ ## 工程起步
18
+
19
+ 用脚手架生成带本 SDK 的完整工程:
20
+
21
+ ```bash
22
+ pnpm create @mbws/plugin my-tool
23
+ ```
24
+
25
+ ## 版本与平台受控构建
26
+
27
+ 平台在线构建时按任务钉定的版本经票据下发 tarball(版本受控,不跟 registry 走);
28
+ npm 上的本包供插件开发者本地开发与类型提示用。
@@ -0,0 +1,99 @@
1
+ /**
2
+ * createMbws() 工厂——SDK 侧协议客户端(设计稿 §6.1 / §6.3 的代码落地)。
3
+ *
4
+ * 时序真话:`mbws` 不是静态导出的对象——端口握手是异步的,插件代码零环境分支:
5
+ * - 生产(宿主 sandbox iframe 内):等宿主经 window.postMessage 递来的 MessagePort
6
+ * (校验 event.source === window.parent,防嵌套 iframe 抢端口)→ init 握手
7
+ * → 按 grants 构造命名空间 → 返回 mbws。
8
+ * - 开发(harness/单测/直接起 dev server):顶窗口或等待端口超时 → dev mock 降级。
9
+ *
10
+ * 传输约定(对 M1 starter 手写引导的收紧):init 经窗口通道递端口是唯一一次
11
+ * window.postMessage;此后所有消息(含 init-ack / panel:* 生命周期)一律走端口。
12
+ *
13
+ * 安全定位:客户端只是包装不是防线——插件绕过客户端手发消息伪造调用,
14
+ * broker 对每条进入的消息仍按 manifest 独立校验(防线在宿主侧)。
15
+ */
16
+ import { type FetchContractOptions, type PluginContract } from "./contract.js";
17
+ import { type MbwsErrorCode } from "./errors.js";
18
+ import type { MbwsErrorPayload } from "./protocol.js";
19
+ /**
20
+ * createMbws() 的返回形态(设计稿 §6.3 命名空间方法面)。
21
+ * 未授予的命名空间在对象上不存在(未装/未授 → 属性 undefined);caps.* 由能力包
22
+ * 动态注册(M3 起),协议固定面不硬编码。
23
+ */
24
+ export interface MbwsClient {
25
+ /** 工人会话数据面:read 读 slot 值 / blob 拿签名 URL / apply 经 write gate 写回 */
26
+ data?: {
27
+ read(key: string): Promise<unknown>;
28
+ blob(key: string): Promise<{
29
+ url: string;
30
+ mime?: string;
31
+ }>;
32
+ apply(outputs: Record<string, unknown>): Promise<void>;
33
+ };
34
+ /** 运行时:进度上报 / 日志 / 取消信号(桥 mbws:event cancel) */
35
+ runtime?: {
36
+ progress(ratio?: number, message?: string): Promise<void>;
37
+ log(...args: unknown[]): Promise<void>;
38
+ signal(): AbortSignal;
39
+ };
40
+ /** per-plugin 配额 KV(落宿主存储,跨宿主刻意不同步) */
41
+ storage?: {
42
+ get(key: string): Promise<unknown>;
43
+ set(key: string, value: unknown): Promise<void>;
44
+ };
45
+ /** 宿主渲染的原生交互件 */
46
+ ui?: {
47
+ confirm(opts: {
48
+ title?: string;
49
+ message: string;
50
+ }): Promise<boolean>;
51
+ toast(message: string, opts?: {
52
+ type?: "info" | "success" | "error";
53
+ }): Promise<void>;
54
+ };
55
+ /** panel 生命周期便利面(panel:resize 等不是 invoke,是独立消息类型) */
56
+ panel: {
57
+ resize(height: number): void;
58
+ /** 注册 before-close 应答者;不注册则宿主超时后按「无未保存」直接关 */
59
+ onBeforeClose(cb: () => void): void;
60
+ /** 应答宿主的 before-close 询问(dirty=true 宿主先弹确认再关) */
61
+ replyBeforeClose(dirty: boolean): void;
62
+ };
63
+ /**
64
+ * 数据结构(契约)面:拉关联任务的最新契约。不经宿主端口——服务端按插件编号
65
+ * (manifest.id)公开派生,dev 预览/宿主内同一调用;故不随 grants 增减、恒在。
66
+ */
67
+ contract: {
68
+ get(opts?: FetchContractOptions): Promise<PluginContract>;
69
+ };
70
+ }
71
+ /** 调用被拒时抛出的规范错误(error.code 是 9 错误码之一) */
72
+ export declare class MbwsInvokeError extends Error {
73
+ readonly code: MbwsErrorCode;
74
+ readonly data?: unknown;
75
+ constructor(payload: MbwsErrorPayload);
76
+ }
77
+ export interface CreateMbwsOptions {
78
+ /** 等宿主 init 端口的超时(ms),超时走 dev mock。默认 3000。 */
79
+ timeoutMs?: number;
80
+ /** 单次 invoke 的默认超时(ms),超时按 E_TIMEOUT 拒绝。默认 30_000。 */
81
+ invokeTimeoutMs?: number;
82
+ }
83
+ /**
84
+ * callId 生成走专用 uuid()(getRandomValues 直拼)——randomUUID 是 secure-context-only,
85
+ * 插件面板经 http 的 registry 代理装载(dev)时不存在,历史上已踩坑。
86
+ */
87
+ /** 分层可测:纯端口逻辑(不碰 window),单测用 Node MessageChannel 直连 broker。 */
88
+ export declare function connectMbwsPort(port: MessagePort, init: {
89
+ grants: string[];
90
+ theme?: Record<string, string>;
91
+ }, opts?: Pick<CreateMbwsOptions, "invokeTimeoutMs">): MbwsClient;
92
+ /**
93
+ * 生产引导的窗口层:等宿主 init(带端口),超时/顶窗口降级 dev mock。
94
+ * 抽成独立函数(接受窗口注入)以支持单测用假 window 驱动。
95
+ */
96
+ export declare function bootstrapFromWindow(win: Pick<Window, "parent" | "addEventListener" | "removeEventListener"> & {
97
+ document?: Document;
98
+ }, opts?: CreateMbwsOptions): Promise<MbwsClient>;
99
+ export declare function createMbws(opts?: CreateMbwsOptions): Promise<MbwsClient>;
package/dist/client.js ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * createMbws() 工厂——SDK 侧协议客户端(设计稿 §6.1 / §6.3 的代码落地)。
3
+ *
4
+ * 时序真话:`mbws` 不是静态导出的对象——端口握手是异步的,插件代码零环境分支:
5
+ * - 生产(宿主 sandbox iframe 内):等宿主经 window.postMessage 递来的 MessagePort
6
+ * (校验 event.source === window.parent,防嵌套 iframe 抢端口)→ init 握手
7
+ * → 按 grants 构造命名空间 → 返回 mbws。
8
+ * - 开发(harness/单测/直接起 dev server):顶窗口或等待端口超时 → dev mock 降级。
9
+ *
10
+ * 传输约定(对 M1 starter 手写引导的收紧):init 经窗口通道递端口是唯一一次
11
+ * window.postMessage;此后所有消息(含 init-ack / panel:* 生命周期)一律走端口。
12
+ *
13
+ * 安全定位:客户端只是包装不是防线——插件绕过客户端手发消息伪造调用,
14
+ * broker 对每条进入的消息仍按 manifest 独立校验(防线在宿主侧)。
15
+ */
16
+ import { fetchContract } from "./contract.js";
17
+ import { createDevMockMbws } from "./dev-mock.js";
18
+ import { MBWS_ERROR_CODES } from "./errors.js";
19
+ import { parseMbwsMessage } from "./protocol.js";
20
+ import { uuid } from "./uuid.js";
21
+ import { PROTOCOL_VERSION, SDK_VERSION } from "./version.js";
22
+ /** 调用被拒时抛出的规范错误(error.code 是 9 错误码之一) */
23
+ export class MbwsInvokeError extends Error {
24
+ code;
25
+ data;
26
+ constructor(payload) {
27
+ super(payload.message ?? payload.code);
28
+ this.name = "MbwsInvokeError";
29
+ this.code = payload.code;
30
+ this.data = payload.data;
31
+ }
32
+ }
33
+ /**
34
+ * callId 生成走专用 uuid()(getRandomValues 直拼)——randomUUID 是 secure-context-only,
35
+ * 插件面板经 http 的 registry 代理装载(dev)时不存在,历史上已踩坑。
36
+ */
37
+ /** 分层可测:纯端口逻辑(不碰 window),单测用 Node MessageChannel 直连 broker。 */
38
+ export function connectMbwsPort(port, init, opts = {}) {
39
+ const invokeTimeoutMs = opts.invokeTimeoutMs ?? 30_000;
40
+ const pending = new Map();
41
+ const abortController = new AbortController();
42
+ let beforeCloseCb = null;
43
+ const post = (message) => {
44
+ port.postMessage(message);
45
+ };
46
+ const invoke = (ns, method, args) => new Promise((resolve, reject) => {
47
+ const callId = uuid();
48
+ const timer = setTimeout(() => {
49
+ pending.delete(callId);
50
+ reject(new MbwsInvokeError({
51
+ code: MBWS_ERROR_CODES.E_TIMEOUT,
52
+ message: `${ns}.${method} 调用超时`,
53
+ }));
54
+ }, invokeTimeoutMs);
55
+ pending.set(callId, { resolve, reject, timer });
56
+ post({
57
+ v: PROTOCOL_VERSION,
58
+ type: "mbws:invoke",
59
+ callId,
60
+ ns,
61
+ method,
62
+ ...(args !== undefined ? { args } : {}),
63
+ });
64
+ });
65
+ const has = (ns) => init.grants.includes(ns);
66
+ port.addEventListener("message", (event) => {
67
+ const message = parseMbwsMessage(event.data);
68
+ if (!message)
69
+ return;
70
+ switch (message.type) {
71
+ case "mbws:resolve": {
72
+ const call = pending.get(message.callId);
73
+ if (call) {
74
+ pending.delete(message.callId);
75
+ clearTimeout(call.timer);
76
+ call.resolve(message.value);
77
+ }
78
+ break;
79
+ }
80
+ case "mbws:reject": {
81
+ const call = pending.get(message.callId);
82
+ if (call) {
83
+ pending.delete(message.callId);
84
+ clearTimeout(call.timer);
85
+ call.reject(new MbwsInvokeError(message.error));
86
+ }
87
+ break;
88
+ }
89
+ case "mbws:event": {
90
+ // 取消事件桥到 AbortSignal;progress/log/theme 由宿主按需下发,客户端不消费
91
+ if (message.event === "cancel")
92
+ abortController.abort();
93
+ break;
94
+ }
95
+ case "panel:before-close": {
96
+ beforeCloseCb?.();
97
+ break;
98
+ }
99
+ default:
100
+ break;
101
+ }
102
+ });
103
+ port.start();
104
+ // 握手次序:theme 已由窗口层写 :root → 端口回 init-ack → 端口发 panel:ready
105
+ post({ v: PROTOCOL_VERSION, type: "mbws:init-ack", sdkVersion: SDK_VERSION });
106
+ post({ v: PROTOCOL_VERSION, type: "panel:ready" });
107
+ return {
108
+ ...(has("data")
109
+ ? {
110
+ data: {
111
+ read: (key) => invoke("data", "read", { key }),
112
+ blob: (key) => invoke("data", "blob", { key }),
113
+ apply: (outputs) => invoke("data", "apply", { outputs }),
114
+ },
115
+ }
116
+ : {}),
117
+ ...(has("runtime")
118
+ ? {
119
+ runtime: {
120
+ progress: (ratio, message) => invoke("runtime", "progress", { ratio, message }),
121
+ log: (...args) => invoke("runtime", "log", { args }),
122
+ signal: () => abortController.signal,
123
+ },
124
+ }
125
+ : {}),
126
+ ...(has("storage")
127
+ ? {
128
+ storage: {
129
+ get: (key) => invoke("storage", "get", { key }),
130
+ set: (key, value) => invoke("storage", "set", { key, value }),
131
+ },
132
+ }
133
+ : {}),
134
+ ...(has("ui")
135
+ ? {
136
+ ui: {
137
+ confirm: (confirmOpts) => invoke("ui", "confirm", confirmOpts),
138
+ toast: (message, toastOpts) => invoke("ui", "toast", { message, ...toastOpts }),
139
+ },
140
+ }
141
+ : {}),
142
+ panel: {
143
+ resize: (height) => post({ v: PROTOCOL_VERSION, type: "panel:resize", height }),
144
+ onBeforeClose(cb) {
145
+ beforeCloseCb = cb;
146
+ },
147
+ replyBeforeClose: (dirty) => post({ v: PROTOCOL_VERSION, type: "panel:before-close-reply", dirty }),
148
+ },
149
+ // 契约面与端口无关(HTTP 直连),端口客户端/dev mock 同形,插件代码零分支
150
+ contract: {
151
+ get: (opts = {}) => fetchContract(opts),
152
+ },
153
+ };
154
+ }
155
+ /**
156
+ * 生产引导的窗口层:等宿主 init(带端口),超时/顶窗口降级 dev mock。
157
+ * 抽成独立函数(接受窗口注入)以支持单测用假 window 驱动。
158
+ */
159
+ export function bootstrapFromWindow(win, opts = {}) {
160
+ const timeoutMs = opts.timeoutMs ?? 3000;
161
+ // 顶窗口(不可能有宿主递端口)→ 直接 dev mock
162
+ if (win.parent === win) {
163
+ return Promise.resolve(createDevMockMbws());
164
+ }
165
+ return new Promise((resolve) => {
166
+ let settled = false;
167
+ const finish = (client) => {
168
+ if (settled)
169
+ return;
170
+ settled = true;
171
+ win.removeEventListener("message", onMessage);
172
+ clearTimeout(timer);
173
+ resolve(client);
174
+ };
175
+ const timer = setTimeout(() => finish(createDevMockMbws()), timeoutMs);
176
+ const onMessage = (event) => {
177
+ // 防窃听伪造:只认直接父窗口递来的 init + 端口(设计稿 §6.1)
178
+ if (event.source !== win.parent)
179
+ return;
180
+ const message = parseMbwsMessage(event.data);
181
+ if (message?.type !== "mbws:init" || !event.ports?.[0])
182
+ return;
183
+ const port = event.ports[0];
184
+ // 主题 token 经 init 下发,写入 :root 跟随宿主主题(设计稿 §9.3)
185
+ if (message.theme && win.document) {
186
+ for (const [name, value] of Object.entries(message.theme)) {
187
+ win.document.documentElement.style.setProperty(name, value);
188
+ }
189
+ }
190
+ finish(connectMbwsPort(port, { grants: message.grants, theme: message.theme }, opts));
191
+ };
192
+ win.addEventListener("message", onMessage);
193
+ });
194
+ }
195
+ /** 一次性守卫:与 VS Code acquireVsCodeApi 同心智,二次调用一律拒绝。 */
196
+ let acquired = false;
197
+ export async function createMbws(opts = {}) {
198
+ if (acquired) {
199
+ throw new MbwsInvokeError({
200
+ code: MBWS_ERROR_CODES.E_PROTOCOL_VIOLATION,
201
+ message: "createMbws() 只能调用一次(与 acquireVsCodeApi 同心智)",
202
+ });
203
+ }
204
+ acquired = true;
205
+ const win = typeof window !== "undefined" ? window : undefined;
206
+ // 无 window(纯 node)也走 dev mock(bootstrapFromWindow 内含顶窗口分支)
207
+ if (!win)
208
+ return createDevMockMbws();
209
+ return bootstrapFromWindow(win, opts);
210
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * mbws.config.ts 的类型与派生——插件工程的配置源(vite.config.ts 同款姿势)。
3
+ *
4
+ * 插件工程根放 `mbws.config.ts`:`export default defineConfig({...})`,TS 类型约束
5
+ * 可填字段(多余字段直接编译报错),动态开发配置(devKey / 后端地址 / 插件自有
6
+ * KV)与静态装载合同同住一个文件但**不进产物**——build 时经 buildManifest 派生
7
+ * manifest.json(宿主装载合同),配置本身不出现在 dist 里。
8
+ *
9
+ * 与 manifest 的分工:manifest.json 是 build 派生物(不是手写源),只含静态合同
10
+ * 面;I/O 契约两头都不落(服务端按编号实时派生,见 contract.ts)。
11
+ */
12
+ import { z } from "zod";
13
+ import { type Manifest } from "./manifest.js";
14
+ /**
15
+ * 用户可填面 = manifest 静态合同 - 派生字段(artifactVersion/sdk 由构建注入)
16
+ * - 契约快照(inputs/outputs,服务端才是权威,不给本地副本留口子)。
17
+ */
18
+ declare const UserConfigSchema: z.ZodObject<{
19
+ id: z.ZodString;
20
+ version: z.ZodString;
21
+ kind: z.ZodEnum<{
22
+ producer: "producer";
23
+ validator: "validator";
24
+ }>;
25
+ paramsSchema: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodType<string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | {
26
+ [key: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
27
+ } | null)[] | {
28
+ [key: string]: string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null;
29
+ } | null)[] | {
30
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
31
+ } | null)[] | {
32
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
33
+ } | null)[] | {
34
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
35
+ } | null)[] | {
36
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
37
+ } | null)[] | {
38
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
39
+ } | null)[] | {
40
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
41
+ } | null)[] | {
42
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
43
+ } | null)[] | {
44
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
45
+ } | null)[] | {
46
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
47
+ } | null)[] | {
48
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
49
+ } | null, unknown, z.core.$ZodTypeInternals<string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | {
50
+ [key: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
51
+ } | null)[] | {
52
+ [key: string]: string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null;
53
+ } | null)[] | {
54
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
55
+ } | null)[] | {
56
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
57
+ } | null)[] | {
58
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
59
+ } | null)[] | {
60
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
61
+ } | null)[] | {
62
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
63
+ } | null)[] | {
64
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
65
+ } | null)[] | {
66
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
67
+ } | null)[] | {
68
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
69
+ } | null)[] | {
70
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
71
+ } | null)[] | {
72
+ [key: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null)[] | /*elided*/ any | null;
73
+ } | null, unknown>>>]>>;
74
+ requiresCapabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
75
+ permissions: z.ZodDefault<z.ZodArray<z.ZodString>>;
76
+ automation: z.ZodEnum<{
77
+ batchable: "batchable";
78
+ "interactive-only": "interactive-only";
79
+ }>;
80
+ components: z.ZodOptional<z.ZodObject<{
81
+ panel: z.ZodOptional<z.ZodString>;
82
+ viewer: z.ZodOptional<z.ZodString>;
83
+ }, z.core.$strip>>;
84
+ meta: z.ZodObject<{
85
+ title: z.ZodString;
86
+ description: z.ZodString;
87
+ icon: z.ZodOptional<z.ZodString>;
88
+ vendor: z.ZodOptional<z.ZodString>;
89
+ screenshots: z.ZodOptional<z.ZodArray<z.ZodString>>;
90
+ }, z.core.$strip>;
91
+ devKey: z.ZodOptional<z.ZodString>;
92
+ apiBaseUrl: z.ZodOptional<z.ZodString>;
93
+ plugin: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
94
+ }, z.core.$strip>;
95
+ /** mbws.config.ts 的可填形状(带默认值的字段可省略) */
96
+ export type MbwsUserConfig = z.input<typeof UserConfigSchema>;
97
+ /** 动态开发配置三件(全部可选、不进产物)——交集回传让消费侧(vite.config 等)
98
+ * 在配置未写这些字段时类型上仍可安全访问到 undefined */
99
+ type MbwsDynamicConfig = Pick<MbwsUserConfig, "devKey" | "apiBaseUrl" | "plugin">;
100
+ /**
101
+ * mbws.config.ts 的入口:原样回传(类型与 IDE 补全由 TS 泛型提供),
102
+ * 运行时再过一遍 zod 校验兜底(防手滑/模板生成物),坏字段直接抛错。
103
+ */
104
+ export declare function defineConfig<T extends MbwsUserConfig>(config: T): T & MbwsDynamicConfig;
105
+ /** 导出给构建脚本/测试做独立校验(defineConfig 之外的编程入口) */
106
+ export declare function validateUserConfig(input: unknown): MbwsUserConfig;
107
+ /**
108
+ * config → 装载合同 manifest:注入 artifactVersion,剥掉全部动态开发字段,
109
+ * 补默认值后产出宿主装载所需的完整合同。build 写 dist、dev 虚拟供给共用此派生。
110
+ */
111
+ export declare function buildManifest(config: MbwsUserConfig): Manifest;
112
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * mbws.config.ts 的类型与派生——插件工程的配置源(vite.config.ts 同款姿势)。
3
+ *
4
+ * 插件工程根放 `mbws.config.ts`:`export default defineConfig({...})`,TS 类型约束
5
+ * 可填字段(多余字段直接编译报错),动态开发配置(devKey / 后端地址 / 插件自有
6
+ * KV)与静态装载合同同住一个文件但**不进产物**——build 时经 buildManifest 派生
7
+ * manifest.json(宿主装载合同),配置本身不出现在 dist 里。
8
+ *
9
+ * 与 manifest 的分工:manifest.json 是 build 派生物(不是手写源),只含静态合同
10
+ * 面;I/O 契约两头都不落(服务端按编号实时派生,见 contract.ts)。
11
+ */
12
+ import { z } from "zod";
13
+ import { ManifestSchema } from "./manifest.js";
14
+ import { ARTIFACT_VERSION } from "./version.js";
15
+ /**
16
+ * 用户可填面 = manifest 静态合同 - 派生字段(artifactVersion/sdk 由构建注入)
17
+ * - 契约快照(inputs/outputs,服务端才是权威,不给本地副本留口子)。
18
+ */
19
+ const UserConfigSchema = ManifestSchema.omit({
20
+ artifactVersion: true,
21
+ sdk: true,
22
+ inputs: true,
23
+ outputs: true,
24
+ }).extend({
25
+ /** 开发者 key(预留:认领发放的开发验证凭证,契约拉取 X-Dev-Key 用)——不进产物 */
26
+ devKey: z.string().optional(),
27
+ /**
28
+ * 后端地址(插件工程里后端地址的唯一配置位):dev 预览的 vite 代理目标,
29
+ * 契约拉取经同源代理转发到这里。SDK/模板不固化任何具体地址——地址是部署事实。
30
+ * 不进产物。
31
+ */
32
+ apiBaseUrl: z.string().optional(),
33
+ /** 插件自有配置(自由 KV,构建脚本/工具链自取;宿主不感知)——不进产物 */
34
+ plugin: z.record(z.string(), z.unknown()).optional(),
35
+ });
36
+ /**
37
+ * mbws.config.ts 的入口:原样回传(类型与 IDE 补全由 TS 泛型提供),
38
+ * 运行时再过一遍 zod 校验兜底(防手滑/模板生成物),坏字段直接抛错。
39
+ */
40
+ export function defineConfig(config) {
41
+ const parsed = UserConfigSchema.safeParse(config);
42
+ if (!parsed.success) {
43
+ const detail = parsed.error.issues
44
+ .map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`)
45
+ .join(";");
46
+ throw new Error(`mbws.config 无效:${detail}`);
47
+ }
48
+ return config;
49
+ }
50
+ /** 导出给构建脚本/测试做独立校验(defineConfig 之外的编程入口) */
51
+ export function validateUserConfig(input) {
52
+ return UserConfigSchema.parse(input);
53
+ }
54
+ /**
55
+ * config → 装载合同 manifest:注入 artifactVersion,剥掉全部动态开发字段,
56
+ * 补默认值后产出宿主装载所需的完整合同。build 写 dist、dev 虚拟供给共用此派生。
57
+ */
58
+ export function buildManifest(config) {
59
+ const { devKey: _devKey, apiBaseUrl: _apiBaseUrl, plugin: _plugin, ...contract } = UserConfigSchema.parse(config);
60
+ return ManifestSchema.parse({ ...contract, artifactVersion: ARTIFACT_VERSION });
61
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 契约动态获取 —— 插件凭自己的编号(manifest.id)直连服务端拉关联任务的最新契约。
3
+ *
4
+ * 插件 ↔ 数据结构的对应关系在服务端维护(PluginDevTask.plugin_code 即关联记录):
5
+ * 认领任务时发编号,manifest.id 对齐该编号;线上契约随需求变动时,再次拉取即得
6
+ * 最新值(updatedAt 作粗版本,供漂移自检:对比 manifest.outputs 键集发现过期)。
7
+ *
8
+ * 传输走普通 HTTP(Node ≥18 / 浏览器全局 fetch),不经宿主端口——开发期/工具链/
9
+ * 面板 dev 预览均可直接用。开发者 key(开发验证凭证)预留请求头位,服务端校验
10
+ * 后续接入。
11
+ */
12
+ /** 服务端返回的任务契约(GET /api/plugin-dev-tasks/contract/by-plugin/{code}) */
13
+ export interface PluginContract {
14
+ /** 契约 id(action_id,数据结构 id——需求侧标识) */
15
+ contractId: string;
16
+ title: string;
17
+ consumes: string[];
18
+ /** inputKey → JSON Schema */
19
+ inputs: Record<string, unknown>;
20
+ /** outputKey → JSON Schema(write gate 的对齐基准) */
21
+ outputs: Record<string, unknown>;
22
+ status: string;
23
+ /** 更新时间(ISO;粗版本,契约漂移检测用) */
24
+ updatedAt: string;
25
+ }
26
+ export interface FetchContractOptions {
27
+ /** 插件编号(= manifest.id);缺省从本工程 manifest.json 读取(浏览器/dev server 场景) */
28
+ pluginCode?: string;
29
+ /**
30
+ * 后端地址;缺省**同源相对**(baseUrl = ""):dev 预览经插件工程 vite 代理转发
31
+ * 到 mbws.config.ts 的 apiBaseUrl(免 CORS——插件 dev 端口不在后端 CORS 白名单);
32
+ * 宿主内网络面由 CSP 锁定,契约拉取本就是 dev/工具链关注点;跨环境工具链
33
+ * (node/CLI)显式传。SDK 不内置任何具体后端地址——地址是部署事实,归配置管。
34
+ */
35
+ baseUrl?: string;
36
+ /** 预留:开发验证凭证,服务端校验后续接入 */
37
+ devKey?: string;
38
+ signal?: AbortSignal;
39
+ }
40
+ /** 按插件编号拉关联任务的最新契约。非 2xx 抛带状态码与后端 message 的 Error。 */
41
+ export declare function fetchContract(opts?: FetchContractOptions): Promise<PluginContract>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * 契约动态获取 —— 插件凭自己的编号(manifest.id)直连服务端拉关联任务的最新契约。
3
+ *
4
+ * 插件 ↔ 数据结构的对应关系在服务端维护(PluginDevTask.plugin_code 即关联记录):
5
+ * 认领任务时发编号,manifest.id 对齐该编号;线上契约随需求变动时,再次拉取即得
6
+ * 最新值(updatedAt 作粗版本,供漂移自检:对比 manifest.outputs 键集发现过期)。
7
+ *
8
+ * 传输走普通 HTTP(Node ≥18 / 浏览器全局 fetch),不经宿主端口——开发期/工具链/
9
+ * 面板 dev 预览均可直接用。开发者 key(开发验证凭证)预留请求头位,服务端校验
10
+ * 后续接入。
11
+ */
12
+ function injectedPluginCode() {
13
+ return typeof __MBWS_PLUGIN_CODE__ !== "undefined" ? __MBWS_PLUGIN_CODE__ : undefined;
14
+ }
15
+ /** manifest.id 按 baseURI 记忆化——同一页面重复调用不再多发 manifest 请求 */
16
+ const manifestIdCache = new Map();
17
+ /** 从本工程根读 manifest.id(dev server / 解包产物内 manifest.json 均在 baseURI 根) */
18
+ async function readManifestId() {
19
+ try {
20
+ const cached = manifestIdCache.get(document.baseURI);
21
+ if (cached)
22
+ return cached;
23
+ const response = await fetch(new URL("manifest.json", document.baseURI));
24
+ if (!response.ok)
25
+ throw new Error(`manifest.json ${response.status}`);
26
+ const manifest = (await response.json());
27
+ if (typeof manifest.id === "string" && manifest.id) {
28
+ manifestIdCache.set(document.baseURI, manifest.id);
29
+ return manifest.id;
30
+ }
31
+ throw new Error("manifest.id 缺失");
32
+ }
33
+ catch (error) {
34
+ throw new Error(`无法从本工程读取 manifest.id(${error instanceof Error ? error.message : error});` +
35
+ "非浏览器环境请显式传 pluginCode");
36
+ }
37
+ }
38
+ /** 按插件编号拉关联任务的最新契约。非 2xx 抛带状态码与后端 message 的 Error。 */
39
+ export async function fetchContract(opts = {}) {
40
+ const pluginCode = opts.pluginCode ?? injectedPluginCode() ?? (await readManifestId());
41
+ const baseUrl = (opts.baseUrl ?? "").replace(/\/+$/, "");
42
+ const headers = {};
43
+ if (opts.devKey)
44
+ headers["X-Dev-Key"] = opts.devKey;
45
+ const response = await fetch(`${baseUrl}/api/plugin-dev-tasks/contract/by-plugin/${encodeURIComponent(pluginCode)}`, { headers, signal: opts.signal });
46
+ if (!response.ok) {
47
+ let message = `契约拉取失败(HTTP ${response.status})`;
48
+ try {
49
+ const body = (await response.json());
50
+ if (typeof body.message === "string" && body.message)
51
+ message = body.message;
52
+ }
53
+ catch {
54
+ // 非 JSON 错误体,保留状态码信息
55
+ }
56
+ throw new Error(message);
57
+ }
58
+ const data = (await response.json());
59
+ return {
60
+ contractId: String(data.contract_id ?? ""),
61
+ title: String(data.title ?? ""),
62
+ consumes: Array.isArray(data.consumes) ? data.consumes.map(String) : [],
63
+ inputs: data.inputs ?? {},
64
+ outputs: data.outputs ?? {},
65
+ status: String(data.status ?? ""),
66
+ updatedAt: String(data.updated_at ?? ""),
67
+ };
68
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * dev mock——开发环境(harness/单测/dev server 直接起)无宿主端口时的降级实现(设计稿 §6.3)。
3
+ *
4
+ * 语义边界:data.read 回 `samples/` 夹具数据;data.apply 只在控制台输出、不入任何库;
5
+ * 其余命名空间 no-op。插件代码零环境分支——同一份代码在 harness 里跑 mock、在宿主里跑真端口。
6
+ */
7
+ import type { MbwsClient } from "./client.js";
8
+ /**
9
+ * 创建 dev mock 客户端。四个固定命名空间全部构造(dev 无授权语义),
10
+ * panel 生命周期面上报/应答均无宿主可收,no-op 即可。
11
+ */
12
+ export declare function createDevMockMbws(): MbwsClient;