@klarkxy/dsh-blueprint 0.1.0-alpha.2
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/ACCEPTANCE.md +33 -0
- package/LICENSE +22 -0
- package/NOTICE.md +14 -0
- package/README.md +106 -0
- package/README.zh-CN.md +85 -0
- package/SETTINGS-SCOPE.md +61 -0
- package/blueprint.mjs +124 -0
- package/client.js +124 -0
- package/codec.mjs +103 -0
- package/cordis.patch.yml +3 -0
- package/engine.mjs +172 -0
- package/index.mjs +33 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/official.mjs +113 -0
- package/package.json +63 -0
package/ACCEPTANCE.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Acceptance record
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-27. Status: implementation/source preview; native end-to-end acceptance is pending.
|
|
4
|
+
|
|
5
|
+
## Executed in this development environment
|
|
6
|
+
|
|
7
|
+
Node.js 22.16.0, Linux, isolated temporary directories.
|
|
8
|
+
|
|
9
|
+
- `npm test`: 84 passed, 0 failed, 0 skipped. Pure-core cases cover strict JSON/J/Z parsing, Unicode and size limits, default inclusion and exclusions, preserved order, two-stage imports, native revision fences, one-use plans, partial failure reporting, connection-target guarding, schema secrets including intersections and new dictionary entries, interruption and final-order verification.
|
|
10
|
+
- Adapter contract fixtures exercise the actual `official.mjs` and `index.mjs` implementations against a synthetic Context with the inspected upstream signatures. They verify redacted native reads, page-independent scope, package/row ownership, malformed policy handling, native install/mutate/cancel arguments, sanitized results and explicit confirmation. They do not load a real DSH host.
|
|
11
|
+
- A VM client-registration test executes `client.js` with a synthetic module loader and React facade. It verifies the official manager page and contextual share action registrations. This is not a browser visual or interaction test.
|
|
12
|
+
- `npm run build`: JavaScript syntax checks of all six executable source files.
|
|
13
|
+
- Distribution contents checked with `npm pack --dry-run` and a real local `npm pack`; tests and development files are not shipped. The packed core can be imported and its codec round-tripped without third-party dependencies.
|
|
14
|
+
|
|
15
|
+
The alpha.2 scope revision adds ten tests: custom-only storage is not read, plugin-list sharing still works, unsupported imported settings block before writes, stale selections fail, author policy cannot widen native access, inactive/mismatched entries are excluded, browser page metadata is rejected, native defaults remain shareable, and the client does not enumerate configuration slots. Existing presentation tests now verify native support regardless of page slots or `autoGenerate`.
|
|
16
|
+
|
|
17
|
+
No real `~/.dsh`, model provider, credential store or network installation was touched.
|
|
18
|
+
|
|
19
|
+
## GitHub Actions verification
|
|
20
|
+
|
|
21
|
+
Implementation commit: `420336f78444ea7e02db190e2c2676c848a78171`.
|
|
22
|
+
|
|
23
|
+
- [CI #42, push](https://github.com/klarkxy/dsh-plugins/actions/runs/36299619992) completed successfully. Its complete job log was inspected: Node.js 24.21.0, pnpm 10.29.2, `pnpm install --frozen-lockfile` and the full root `pnpm check` passed. The blueprint package ran all 84 tests with zero failures and zero skips. Release-target checks, site tests, existing TypeScript checks, other plugin tests, build and pack checks also passed.
|
|
24
|
+
- [CI #43, pull request](https://github.com/klarkxy/dsh-plugins/actions/runs/36299637488) also completed successfully; its job steps confirm the frozen installation and full root check.
|
|
25
|
+
- The package added no dependencies and did not change the root lockfile, workflows or unrelated packages. All 18 initial remote blobs were checked against the tested local source, tests and documentation. This record is a later documentation-only addition; the linked runs identify the implementation commit they actually tested.
|
|
26
|
+
|
|
27
|
+
## Not established by those tests
|
|
28
|
+
|
|
29
|
+
The local shell cannot resolve GitHub and pnpm is not installed; dependency installation and the root monorepo checks were not run locally. They were verified in CI as recorded above, not by claiming a local run. No claim is made that a particular released DSH npm/desktop build has passed the whole workflow. Genuine HTTP authentication, HMR/plugin lifecycle behavior, cross-editor races and a real browser remain to be checked in a configured runtime.
|
|
30
|
+
|
|
31
|
+
Before a stable release, use a fresh throwaway DSH Home and a non-production profile to install the actual tarball, verify Plugins-page mounting, export/import a real npm bundle in both modes, inspect bundle order, confirm secrets remain local, exercise post-install configuration preview and native restart/failure cases, and test browser reload/cancellation. Repeat on supported hosts. Do not claim a green unit-test run covers these steps.
|
|
32
|
+
|
|
33
|
+
The package remains a prerelease; it is not added to the site's npm-backed catalog until a published `latest` version exists. This change does not remove the source repository's v1 runtime, delete user blueprints, merge a PR or publish to npm.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DSH Spaces contributors
|
|
4
|
+
Copyright (c) 2026 klarkxy
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Source provenance
|
|
2
|
+
|
|
3
|
+
The JSON/share-code blueprint concept and the raw-DEFLATE, Base64url, UTF-8 and bounded-decompression codec in `codec.mjs` are extracted/adapted from:
|
|
4
|
+
|
|
5
|
+
- Repository: https://github.com/klarkxy/dsh-spaces
|
|
6
|
+
- Source path: `src/adapters/node/blueprint-codec.ts`
|
|
7
|
+
- Source blob: `c3e9a717dcf00ce1a7bab68dd53e7675c59712d2`
|
|
8
|
+
- Source license: MIT, copyright (c) 2026 DSH Spaces contributors.
|
|
9
|
+
|
|
10
|
+
The original MIT notice is retained in LICENSE. New code in this package is also MIT. The surrounding dsh-plugins repository's other packages retain their own licenses.
|
|
11
|
+
|
|
12
|
+
The application engine, strict v2 JSON validation, optional policy, host adapter and client integration here target the official current-profile plugin manager. Spaces Supervisor, space creation, LLM connection bindings, workbench RPC, Electron integration and private filesystem write paths are not runtime dependencies and were not copied.
|
|
13
|
+
|
|
14
|
+
Upstream API inspection: deepseek-ai/deepseek-harness at `477b4f420553e8a52c2fbccc464d7561b239c443`, particularly `packages/boot/plugin-manager`, `packages/settings/settings`, `packages/client/connection`, `packages/client/ui-plugin-manager` and the official settings-page controllers. This inspection is source evidence, not a claim that the same APIs are shipped by every npm or desktop version.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# DSH Blueprint
|
|
2
|
+
|
|
3
|
+
[中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Export and import DSH plugin compositions from the **official Plugins page**. Share either the plugin list and layer order, or the same composition with saved configuration. No Spaces installation, workspace supervisor, separate desktop application or cloud account is required.
|
|
6
|
+
|
|
7
|
+
**Source preview, not a published or end-to-end-certified release.** See [ACCEPTANCE.md](ACCEPTANCE.md) for actual test coverage and outstanding runtime verification.
|
|
8
|
+
|
|
9
|
+
## Install this checkout
|
|
10
|
+
|
|
11
|
+
Requires a DSH Web host exposing the official `pluginManager`, profile-backed volatile `settings`, `pluginPackages`, `loader`, authenticated `connection.rpc` and plugin-manager configuration slots. The adapter checks capabilities instead of assuming a version string guarantees support. The current source contracts were reviewed at upstream commit `477b4f420553e8a52c2fbccc464d7561b239c443`; compatibility with a particular npm/desktop release still needs a runtime test.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
cd plugins/dsh-blueprint
|
|
15
|
+
npm test
|
|
16
|
+
npm run build
|
|
17
|
+
npm pack
|
|
18
|
+
# Use the tarball path printed by npm pack, and your intended profile:
|
|
19
|
+
dsh plugin --profile <target-profile> add /absolute/path/to/klarkxy-dsh-blueprint-0.1.0-alpha.2.tgz
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This package has no npm dependencies and no compilation requirement. It calls native host services and uses the host's React runtime. Do not copy a standalone React or another Cordis instance into the host.
|
|
23
|
+
|
|
24
|
+
## Use
|
|
25
|
+
|
|
26
|
+
Open **Plugins → DSH Blueprint**. A **Share blueprint** action on another bundle's detail page opens the same export screen focused on that bundle.
|
|
27
|
+
|
|
28
|
+
Select bundles, choose **Plugins only** or **Include saved settings**, and generate a preview. Settings mode also records editable plugin-row enabled states. Ordinary fields are included by default. Uncheck a form or individual field before generating. Copy the share code or save UTF-8 JSON. Nothing is published or sent to a third-party service.
|
|
29
|
+
|
|
30
|
+
On the receiving host, paste a code/JSON or open a file, then **Preview import changes**. Inspect versions, operations, the resulting complete layer order, settings values and warnings. Confirm the trust notice before applying. Missing packages are installed disabled through the official manager; requested bundle activation is an explicit, previewed operation and executes plugin code.
|
|
31
|
+
|
|
32
|
+
When composition changes are needed, apply those first, then run **Preview settings after installation**. This second preview binds configuration writes to the real installed plugin's native schema and revision. The plugin never invents configuration metadata for code that has not been loaded. Native `restart-required`, rejected writes or failures stop the plan and are reported rather than called success.
|
|
33
|
+
|
|
34
|
+
## Scope and defaults
|
|
35
|
+
|
|
36
|
+
- Only bundles listed by the official manager are selected. Exact npm versions and installation-provided bundles are supported. Local links, git/tarball sources, aliases, unreadable metadata and version conflicts are reported, not silently rewritten as npm packages. Dependencies without a bundle remain the native package manager's responsibility.
|
|
37
|
+
- The native manifest supplies layer order, not the alphabetically sorted cards. Unshared bundles are not removed or disabled. If relative order must change, selected active bundles are disabled and then enabled in blueprint order, after unshared layers. The preview shows this placement and every resulting operation. A bundle with no activation change is not cycled unnecessarily.
|
|
38
|
+
- Settings come exclusively from the official Config/Settings **native editable descriptors**: saved effective values, including defaults, not browser drafts, raw Config files or plugin data directories. No page ledger or per-page mapping is maintained. Custom UI over native Settings works without UI-specific handling; custom-only pages, APIs and storage are deliberately unsupported.
|
|
39
|
+
- Only active, uniquely owned, writable native forms are eligible. Ordinary nonvolatile fields are not shared. Unavailable native settings are reported; package-list sharing remains available. An incoming unsupported form or path blocks the settings preview instead of being silently dropped. A custom page may display fewer fields than the native descriptor: review the actual export fields, not the page's appearance.
|
|
40
|
+
- See [Settings scope](SETTINGS-SCOPE.md) for the normative boundary, author responsibilities, alpha.1 behavior change and upstream evidence. There is no planned per-plugin custom-storage adapter or migration layer.
|
|
41
|
+
- Host-redacted secrets never enter the blueprint. A secret-bearing array is omitted as a whole rather than reindexed or copied incompletely. The plugin does not read the credential service. **Unmarked private text is not reliably recognizable as a secret.** Preview the output before publishing it.
|
|
42
|
+
- Import applies explicit field edits with the native revision check, preserving unmentioned values. It is not a whole-form replacement. Nonsecret arrays are replaced atomically; they are not guessed or merged by index. Connection-target changes (`baseURL`, `endpoint`, `url`, `host`, `apiKeyEnv` and spelling variants) require local changes in the official page before importing, even when the secret lives outside the Settings form. This avoids silently combining an imported endpoint with a receiver's existing key.
|
|
43
|
+
- Native type/schema validation still applies. Directory strings and provider/model route names are ordinary settings, not copied resources; the recipient must ensure they refer to available local resources. Import does not transfer directories, model connections, keys, sessions, attachments, histories, databases or caches.
|
|
44
|
+
|
|
45
|
+
## Optional author policy
|
|
46
|
+
|
|
47
|
+
No contribution means **include the native public form by default**. In the owning package's `package.json`, an author can narrow sharing for an exact native row id:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"dshBlueprint": {
|
|
52
|
+
"version": 1,
|
|
53
|
+
"entries": {
|
|
54
|
+
"my-plugin": {
|
|
55
|
+
"exclude": [["privateNote"], ["connection", "machineId"]]
|
|
56
|
+
},
|
|
57
|
+
"another-instance": { "share": false },
|
|
58
|
+
"theme": { "include": [["appearance"]] }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Paths are arrays of exact segments, not globs or JavaScript. `share: false` excludes the entire form; `include` narrows it; `exclude`, native editability and host secret annotations always win. The receiver rechecks its installed policy. Invalid declarations make that package's configuration unavailable with a warning; they never degrade to exporting everything. Metadata is read as JSON without executing export callbacks. This policy only narrows native fields; it cannot register another storage source. This is this plugin's contribution contract, not an official DSH manifest extension.
|
|
65
|
+
|
|
66
|
+
## Portable format v2
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"kind": "dsh-blueprint",
|
|
71
|
+
"formatVersion": 2,
|
|
72
|
+
"metadata": { "name": "My setup", "version": "1.0.0" },
|
|
73
|
+
"packages": [
|
|
74
|
+
{ "name": "example-plugin", "version": "1.2.3", "source": "npm" }
|
|
75
|
+
],
|
|
76
|
+
"bundles": ["example-plugin"],
|
|
77
|
+
"settings": [
|
|
78
|
+
{
|
|
79
|
+
"package": "example-plugin",
|
|
80
|
+
"row": "example",
|
|
81
|
+
"module": "example-plugin",
|
|
82
|
+
"fields": [{ "path": ["theme"], "value": "paper" }]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The package is illustrative, not an install recommendation. `settings` and `rows` are optional; plugin-only exports omit both. `rows` entries carry `package`, `row`, `module` and boolean `enabled`. `source` is `npm` or `builtin`. Exact row/module/package identity is verified on the recipient. Unknown root/entry fields and unsupported versions are rejected.
|
|
89
|
+
|
|
90
|
+
Codes are `DSHBP2:J:<unpadded-base64url-UTF8-JSON>` or `DSHBP2:Z:<unpadded-base64url-raw-DEFLATE-JSON>`. The encoder chooses the shorter form. Limits: 1 MiB decoded JSON, 2 MiB input code, 64 JSON container levels. Duplicate JSON members, unsafe numbers, prototype keys, malformed Unicode, noncanonical Base64url and trailing compressed streams are rejected. Codes are **not encryption, signatures or proof of trust**.
|
|
91
|
+
|
|
92
|
+
Spaces `DSHBP1` / format v1 described creation of a new isolated space, including Spaces-specific bindings. The v2 reader rejects those codes rather than silently applying them to the current profile. Re-export through the new manager integration. This extraction does not delete or rewrite the legacy v1 implementation in the source repository; no in-place v1 migration is claimed.
|
|
93
|
+
|
|
94
|
+
## Operation and security contract
|
|
95
|
+
|
|
96
|
+
All management goes through the official manager; all setting writes go through native Settings. The package registers an authenticated logical RPC channel through official Connection, not a second server. The native transport's request cap also applies.
|
|
97
|
+
|
|
98
|
+
A preview creates a process-local, five-minute, one-use plan bound to the observed profile and form revisions. At most eight plans/results are retained. Changed observations invalidate a plan; completed results can be read without rerunning operations. No automatic retries, upgrades, downgrades, script approvals, removals, cross-profile writes or blueprint-level rollback are performed. The native installer retains its own documented failure behavior.
|
|
99
|
+
|
|
100
|
+
A multi-step import is **not a transaction or a lock against every other editor**. It checks observed state between operations and uses native per-operation serialization/revision checks. Stop editing that profile elsewhere while applying. Failures preserve visible partial outcomes; a lost response is not a reason to replay an import. Closing the page requests cancellation where supported. Already-completed changes may remain.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
`npm test` runs dependency-free unit, adapter-contract and client-registration tests. `npm run build` checks JavaScript syntax; it is not a TypeScript or native integration check. `npm pack --dry-run` checks distribution contents. No test uses the real DSH home.
|
|
105
|
+
|
|
106
|
+
MIT. The extracted codec's original copyright is retained; see [NOTICE.md](NOTICE.md) and [LICENSE](LICENSE).
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# DSH 蓝图
|
|
2
|
+
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
在 **DSH 官方插件管理器**内导入、导出插件组合:可以只分享插件列表与生效顺序,也可以连已保存的配置一起分享。无需安装 Spaces,无需独立客户端、Supervisor 或云账户。
|
|
6
|
+
|
|
7
|
+
**当前为源码预览版本,尚未发布到 npm,也未完成真实 DSH 端到端验收。** 已执行检查与剩余验收见 [ACCEPTANCE.md](ACCEPTANCE.md)。
|
|
8
|
+
|
|
9
|
+
## 安装与入口
|
|
10
|
+
|
|
11
|
+
需要提供官方 `pluginManager`、基于 profile 的 volatile `settings`、`pluginPackages`、`loader`、已鉴权 `connection.rpc` 和插件配置 slot 的 DSH Web 宿主。启动会检查所需能力;仅凭版本号不声称兼容。接口核对基于上游提交 `477b4f420553e8a52c2fbccc464d7561b239c443`,具体 npm 或桌面版本仍需实际验收。
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
cd plugins/dsh-blueprint
|
|
15
|
+
npm test
|
|
16
|
+
npm run build
|
|
17
|
+
npm pack
|
|
18
|
+
# 使用 npm pack 输出的归档路径,并明确目标 profile:
|
|
19
|
+
dsh plugin --profile <目标profile> add /绝对路径/klarkxy-dsh-blueprint-0.1.0-alpha.2.tgz
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
包没有 npm 依赖和编译要求,复用宿主服务与 React。安装后打开 **插件 → DSH 蓝图**。其他组合包详情页的 **分享蓝图** 按钮会进入同一界面并选中该包。
|
|
23
|
+
|
|
24
|
+
## 导出
|
|
25
|
+
|
|
26
|
+
选择插件,选择 **仅插件组合** 或 **包含已保存设置**。后者还记录可修改的插件行启停状态。普通配置默认包含,不要求作者专门适配;生成前可取消某个表单或字段。检查生成的 JSON,然后复制分享码或保存 JSON。不会自动上传或发布。
|
|
27
|
+
|
|
28
|
+
**唯一设置来源是官方 Config/Settings 暴露的可编辑描述符。** 导出原生已保存的生效值(包括默认值),不读取浏览器草稿、普通非 volatile Config 字段、任意配置文件或插件数据目录。不再枚举页面 slots,也不维护页面专用字段映射。
|
|
29
|
+
|
|
30
|
+
自己画 UI、但仍使用原生 Settings 的插件,在数据层照常支持;完全自建页面、接口或存储的配置不读取、不推断、不适配。自定义页面可能只显示原生字段的子集,蓝图不尝试识别该子集;请按实际导出预览选择字段。不可用的原生设置会报告,但插件清单仍可分享;导入不支持的设置会阻止设置预览,不默默丢弃。
|
|
31
|
+
|
|
32
|
+
完整范围、作者责任、与 alpha.1 的行为区别及官方依据见 [设置范围说明](SETTINGS-SCOPE.md)。自建配置适配与自动迁移明确不在本产品范围内。
|
|
33
|
+
|
|
34
|
+
关闭的插件没有活动表单,不能读取其不可见设置,但仍可分享安装清单与 bundle 选择。共享前请核对列出的配置项。凭据服务不被读取;宿主已标记的秘密不会导出。数组含秘密或排除字段时,整段数组被排除,避免索引错位。**未标记的私密文本不能保证被自动识别,公开分享前应查看预览。**
|
|
35
|
+
|
|
36
|
+
## 导入
|
|
37
|
+
|
|
38
|
+
粘贴分享码、JSON 或读取文件,选择 **预览导入变更**,核对版本、操作、完整生效顺序和设置值,再确认信任提示并执行。安装、启停都调用官方管理器,不另建安装器;不自动授权构建脚本,但宿主已有的脚本授权仍可能生效。启用插件会执行其代码。
|
|
39
|
+
|
|
40
|
+
需要新增或重排组合时,先执行组合阶段,再点 **安装后重新预览设置**。第二次预览依据真正安装后的原生 schema 和修订号生成设置操作。宿主要求重启、配置拒绝或中途失败都会停止,不会报告为整体成功。
|
|
41
|
+
|
|
42
|
+
只修改所选组合包,不卸载未选包。需要改变相对顺序时,先关闭所选已启用层,再按蓝图顺序开启,放到未选层之后;预览会展示全部操作和最终顺序。无须重排时不重复启停。安装清单与生效顺序分别记录,不使用页面的字母排序冒充顺序。
|
|
43
|
+
|
|
44
|
+
设置采用带原生 revision 的逐字段写入,保留未提及的值,不用脱敏后的对象整段覆盖配置。普通数组作为整体替换,不猜测按下标合并。`baseURL`、`endpoint`、`url`、`host`、`apiKeyEnv` 等连接目标发生变化时,需要先在接收者的官方配置页本地处理,再导入其余设置,避免将分享者的地址和接收者的密钥自动配对。
|
|
45
|
+
|
|
46
|
+
目录字符串和模型路由名只是设置值,不代表相关资源被复制;接收者应确认本机可用性。不会搬运模型连接库、API Key、会话、附件、数据库或缓存。
|
|
47
|
+
|
|
48
|
+
## 插件作者声明
|
|
49
|
+
|
|
50
|
+
缺省规则为 **包含对应原生表单内的公开字段**。作者可在自己包的 `package.json` 中按原生插件行 id 声明:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"dshBlueprint": {
|
|
55
|
+
"version": 1,
|
|
56
|
+
"entries": {
|
|
57
|
+
"my-plugin": { "exclude": [["privateNote"], ["connection", "machineId"]] },
|
|
58
|
+
"another-instance": { "share": false },
|
|
59
|
+
"theme": { "include": [["appearance"]] }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
路径是精确段落数组,不是 glob、表达式或 JavaScript。`share: false` 关闭整个表单的分享;`include` 缩小范围;`exclude`、原生可编辑范围和宿主秘密标记优先。接收端重新检查本机安装版本的声明。无效声明会明确使该包的配置不可分享,不退回“全量导出”。声明只读取 JSON,不执行自定义导出回调,也不能注册自建存储来源。这是本插件的扩展约定,不冒充官方 DSH manifest 标准。
|
|
66
|
+
|
|
67
|
+
## 格式与迁移
|
|
68
|
+
|
|
69
|
+
新格式为 `kind: "dsh-blueprint"`、`formatVersion: 2`。`packages` 记录精确版本和 `npm` / `builtin` 来源;`bundles` 记录有序启用层;可选 `settings` 记录 `{package,row,module,fields:[{path,value}]}`;可选 `rows` 记录 `{package,row,module,enabled}`。最小示例及字段规则见英文文档的 Portable format v2。
|
|
70
|
+
|
|
71
|
+
分享码为 `DSHBP2:J:`(UTF-8 JSON 的无填充 Base64url)或 `DSHBP2:Z:`(先 raw DEFLATE 压缩)。最大 JSON 1 MiB、输入分享码 2 MiB、64 层 JSON 容器;宿主传输的请求大小限制也生效。重复键、不安全数字、原型键、损坏 Unicode、非规范 Base64url 和压缩尾随数据拒绝读取。编码不是加密、签名或作者认证。
|
|
72
|
+
|
|
73
|
+
只支持可确认的精确 npm 来源与宿主内置组合包。本地链接、git/tarball、别名和版本冲突会列出,不悄悄改源。官方管理器不列为组合包的普通依赖由包管理器负责。
|
|
74
|
+
|
|
75
|
+
Spaces 的 `DSHBP1` / v1 原义是创建新隔离空间,包含专属绑定;这里明确拒绝,不偷偷把旧码解释成修改当前 profile。应通过新入口重新导出。本次独立实现没有删除或改写原仓库的历史 v1 实现,不宣称已完成旧空间数据迁移。
|
|
76
|
+
|
|
77
|
+
## 执行边界
|
|
78
|
+
|
|
79
|
+
复用官方 Connection 的鉴权与 Host/Origin 检查,不启动第二个服务器。预览计划只保存在当前进程,五分钟过期、单次消费,最多保留八个计划或结果;实际观测变化使旧计划失效。已完成的结果可读取,不重新执行。
|
|
80
|
+
|
|
81
|
+
不自动重试、升级、降级、授权脚本、卸载、跨 profile 写入或整组回滚。原生安装器自己的失败行为保持不变。多步骤导入不是全局事务,也没有锁住所有其他编辑器;执行期间请勿在其他窗口同时改该 profile。中途失败保留已完成结果;响应丢失不表示可以重放。关闭页面会在支持时请求取消,已经完成的更改可能保留。
|
|
82
|
+
|
|
83
|
+
`npm test` 为无外部依赖的单元、接口适配和界面注册测试;`npm run build` 为语法检查,不等于真实宿主或 TypeScript 验收。所有测试只使用临时目录,不接触真实 DSH Home。
|
|
84
|
+
|
|
85
|
+
MIT;原 Spaces 编解码器的版权说明保留在 [NOTICE.md](NOTICE.md) 与 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Settings scope / 设置范围
|
|
2
|
+
|
|
3
|
+
Decision date: 2026-09-27. Applies to `@klarkxy/dsh-blueprint@0.1.0-alpha.2`.
|
|
4
|
+
|
|
5
|
+
## 中文:只对接官方配置契约
|
|
6
|
+
|
|
7
|
+
蓝图以 **官方 Config / Settings 暴露的可编辑字段** 为唯一设置数据来源,不以任意自定义页面的控件或可见字段为准。插件管理器是操作入口,不是所有第三方数据存储的统一协议。
|
|
8
|
+
|
|
9
|
+
| 情况 | 行为 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| 活动插件通过官方 Settings 暴露可唯一定位的 volatile 字段 | 可导出已保存的生效值;导入使用原生逐字段修改与 revision 检查。 |
|
|
12
|
+
| 插件自己画 UI,但仍使用同一官方 Settings 数据接口 | 原生字段照常支持,不分析 UI,也不按页面显示的子集自动裁剪。 |
|
|
13
|
+
| 插件只使用自定义页面、自建接口、文件、localStorage 或数据库 | 不读取、不推断、不适配;其插件清单和可管理的启用状态仍可分享。 |
|
|
14
|
+
| 插件未启动、已禁用或没有原生表单 | 不读取底层 Config 或旧存储作为替代;不可用设置会报告。 |
|
|
15
|
+
| 接收者没有匹配的原生表单或可编辑路径 | 设置预览明确阻止写入,不默默忽略、不创建自定义设置。 |
|
|
16
|
+
|
|
17
|
+
### 数据来源与写入
|
|
18
|
+
|
|
19
|
+
读操作只使用 `settings.describe({ redactSecrets: true })` 返回的原生描述符值,校对官方插件清单、包/行/模块身份和可修改性;只读取 Loader 的 schema 元数据检查 volatile 与 secret 标记,不从运行时 Config 抽取隐藏值。文件读取只用于 profile/package manifest 的组合顺序、包身份和声明式分享策略,不遍历插件设置目录。
|
|
20
|
+
|
|
21
|
+
不再维护 Shell、搜索等页面的手写字段映射,不接收浏览器上传的页面 ledger 作为授权依据,不枚举或解析第三方 React 表单,不安装页面专用导出回调。`autoGenerate: false` 只是呈现策略,不会使仍由官方 Settings 提供的数据失去支持。
|
|
22
|
+
|
|
23
|
+
导入只通过 `settings.mutate` 写入明确的字段路径和读取时的修订号,由 Host 验证完整 Config 并持久化;不整段替换脱敏对象,不修改未提及的设置。非原生路径、秘密路径及作者禁止路径不能因蓝图自行声明而获得权限。
|
|
24
|
+
|
|
25
|
+
### 分享默认值与责任边界
|
|
26
|
+
|
|
27
|
+
没有作者声明时,原生范围内的公开可编辑字段默认包含。`dshBlueprint.entries` 是本插件可选的声明式缩小规则:`exclude`、`include`、`share: false`;它不能扩展到官方数据接口以外,也不是自定义存储的适配器。宿主已标记的秘密始终排除;用户可取消表单或字段。未知的私密文本无法保证自动识别,发布前需要检查预览。
|
|
28
|
+
|
|
29
|
+
“自定义配置不支持”是当前产品的明确范围,不是承诺以后为每个插件补适配。希望参与配置分享的作者应使用官方 Config / Settings 接口;现有自建配置的迁移由插件作者处理,蓝图不提供扫描、备份或自动迁移。
|
|
30
|
+
|
|
31
|
+
### 兼容性与失败说明
|
|
32
|
+
|
|
33
|
+
此版本取消 alpha.1 中按页面 slots 选择表单和四组手写页面字段映射的行为;分享范围以官方原生描述符为准,因此同一原生表单可能比某个自定义页面显示的字段更多。导出界面和 JSON 预览必须如实展示字段,用户或作者可以进一步缩小。
|
|
34
|
+
|
|
35
|
+
蓝图仍使用格式 v2 / `DSHBP2`。接收旧的 v2 数据时,所有设置仍需通过当前原生身份、schema、作者策略和秘密检查。未识别的自定义路径会阻止设置阶段。预览、分阶段执行和部分失败报告不提供跨阶段事务保证;安装阶段已经完成的操作不会因后续设置不支持而自动回滚。
|
|
36
|
+
|
|
37
|
+
## English: native configuration contract only
|
|
38
|
+
|
|
39
|
+
The sole settings source is the **editable data exposed by official Config / Settings**, not the controls or visible subset of an arbitrary custom page. The official plugin manager supplies the user entry point; registering a UI does not make a third-party storage format part of this contract.
|
|
40
|
+
|
|
41
|
+
Active, uniquely addressable native volatile fields support export of saved effective values and import through revision-fenced path edits. A custom UI that uses the same native Settings contract remains supported at the data level, including when `autoGenerate` is false. No UI inspection or special adapter is needed or promised.
|
|
42
|
+
|
|
43
|
+
Custom-only pages, APIs, local files, browser storage and databases are out of scope. Their packages and manageable activation state remain shareable. Missing/inactive native forms have no raw-Config or filesystem fallback. Incoming settings without an exact native form or editable path block the settings preview instead of being silently ignored or written elsewhere.
|
|
44
|
+
|
|
45
|
+
The reader consumes only `settings.describe({ redactSecrets: true })` values. Native manager records and Loader schema metadata establish identity, writability, volatile fields and secret protection. Loader Config values are not an export source. File reads are limited to profile/package manifests for order, package identity and declarative policy. There is no client-supplied page ledger, hand-maintained field table, DOM/React inspection, storage crawl or page-specific export callback.
|
|
46
|
+
|
|
47
|
+
Writes use official `settings.mutate` with explicit paths and the observed revision. Host validation and persistence stay authoritative. Unmentioned values remain local; a redacted object is never used as a whole-form replacement. A blueprint cannot grant itself access to nonnative or secret paths.
|
|
48
|
+
|
|
49
|
+
No author policy means include the public native editable fields by default. The optional `dshBlueprint.entries` policy can only narrow that set; it cannot grant access to another data source. Host-marked secrets remain excluded, users can deselect fields, and unmarked private text still needs review. Custom-storage adapters and automatic migrations are deliberately not part of this product.
|
|
50
|
+
|
|
51
|
+
This supersedes alpha.1's page-ledger filtering and four hard-coded official page mappings. Native descriptors can contain more fields than one custom UI displays; the export screen and JSON preview expose that scope. Format v2 / `DSHBP2` remains unchanged and imported v2 fields are revalidated against the receiver's native contract. Unsupported settings block their stage; successful earlier package operations are not rolled back.
|
|
52
|
+
|
|
53
|
+
## Upstream evidence / 官方依据
|
|
54
|
+
|
|
55
|
+
Reviewed against upstream commit `477b4f420553e8a52c2fbccc464d7561b239c443`:
|
|
56
|
+
|
|
57
|
+
- [Settings contract](https://github.com/deepseek-ai/deepseek-harness/blob/477b4f420553e8a52c2fbccc464d7561b239c443/packages/settings/settings/README.zh.md): native volatile forms, instance identity, `autoGenerate`, redaction and unavailable forms.
|
|
58
|
+
- [Settings API](https://github.com/deepseek-ai/deepseek-harness/blob/477b4f420553e8a52c2fbccc464d7561b239c443/docs/subsystems/settings.zh.md): descriptors, path edits, complete-Config validation and revision checks.
|
|
59
|
+
- [Plugin-page slots](https://github.com/deepseek-ai/deepseek-harness/blob/477b4f420553e8a52c2fbccc464d7561b239c443/packages/client/ui-plugin-manager/src/client/slot-contract.ts): presentation extension points, not a universal settings-storage declaration.
|
|
60
|
+
|
|
61
|
+
These source contracts are not proof of an end-to-end pass on every released host. See [ACCEPTANCE.md](ACCEPTANCE.md).
|
package/blueprint.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { fail, plain, forbidden, parseJson } from './codec.mjs';
|
|
2
|
+
export const PACKAGE = '@klarkxy/dsh-blueprint';
|
|
3
|
+
const NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
4
|
+
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
5
|
+
export const exactVersion = value => typeof value === 'string' && VERSION.test(value);
|
|
6
|
+
export function closed(value, keys, label) {
|
|
7
|
+
if (!plain(value) || Object.keys(value).some(k => !keys.includes(k))) fail('shape', `Invalid ${label}.`);
|
|
8
|
+
}
|
|
9
|
+
export function text(value, label, max = 512) {
|
|
10
|
+
if (typeof value !== 'string' || !value.length || value.length > max || /[\x00-\x1f]/.test(value)) fail('field', `Invalid ${label}.`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
export function array(value, label, max = 512) {
|
|
14
|
+
if (!Array.isArray(value) || value.length > max) fail('field', `Invalid ${label}.`); return value;
|
|
15
|
+
}
|
|
16
|
+
export function path(value) {
|
|
17
|
+
array(value, 'field path', 64);
|
|
18
|
+
if (!value.length) fail('path', 'Whole-form replacement is not supported.');
|
|
19
|
+
for (const p of value) { text(p, 'path segment'); if (forbidden(p)) fail('path', 'Unsafe field path.'); }
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
export const prefix = (a, b) => a.length <= b.length && a.every((v, i) => v === b[i]);
|
|
23
|
+
export const overlaps = (a, b) => prefix(a, b) || prefix(b, a);
|
|
24
|
+
export const identity = form => JSON.stringify([form.package, form.row, form.module]);
|
|
25
|
+
export const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
26
|
+
export function get(value, parts) {
|
|
27
|
+
for (const part of parts) { if (value === null || typeof value !== 'object' || !Object.hasOwn(value, part)) return undefined; value = value[part]; }
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
export function validate(value) {
|
|
31
|
+
// Also reject cycles, non-finite values, exotic objects and undefined before JSON conversion.
|
|
32
|
+
function json(v, depth = 0) {
|
|
33
|
+
if (depth > 64) fail('depth', 'Blueprint too deeply nested.');
|
|
34
|
+
if (v === null || typeof v === 'string' || typeof v === 'boolean') return;
|
|
35
|
+
if (typeof v === 'number' && Number.isFinite(v) && (!Number.isInteger(v) || Number.isSafeInteger(v))) return;
|
|
36
|
+
if (!Array.isArray(v) && !plain(v)) fail('json', 'Blueprint must contain JSON data only.');
|
|
37
|
+
if (Array.isArray(v) && Object.keys(v).length !== v.length) fail('json', 'Sparse or decorated arrays are not supported.');
|
|
38
|
+
for (const [k, x] of Object.entries(v)) { if (forbidden(k)) fail('key', 'Unsafe object key.'); json(x, depth + 1); }
|
|
39
|
+
}
|
|
40
|
+
json(value); value = parseJson(JSON.stringify(value));
|
|
41
|
+
closed(value, ['kind', 'formatVersion', 'metadata', 'packages', 'bundles', 'settings', 'rows'], 'blueprint');
|
|
42
|
+
if (value.kind !== 'dsh-blueprint' || value.formatVersion !== 2) fail('version', 'Only current-profile dsh-blueprint v2 is supported.');
|
|
43
|
+
closed(value.metadata, ['name', 'version', 'description'], 'metadata'); text(value.metadata.name, 'name', 120);
|
|
44
|
+
if (!exactVersion(value.metadata.version)) fail('version', 'Invalid blueprint release version.');
|
|
45
|
+
if (value.metadata.description !== undefined) text(value.metadata.description, 'description', 4000);
|
|
46
|
+
const names = new Set();
|
|
47
|
+
for (const p of array(value.packages, 'packages', 128)) {
|
|
48
|
+
closed(p, ['name', 'version', 'source'], 'package');
|
|
49
|
+
if (typeof p.name !== 'string' || !NAME.test(p.name) || p.name.length > 214 || p.name === PACKAGE || names.has(p.name)) fail('package', 'Invalid, duplicate or self-referencing package.');
|
|
50
|
+
if (!exactVersion(p.version) || !['npm', 'builtin'].includes(p.source)) fail('package', 'Packages require exact versions and known sources.');
|
|
51
|
+
names.add(p.name);
|
|
52
|
+
}
|
|
53
|
+
const bundles = array(value.bundles, 'bundles', 128);
|
|
54
|
+
if (new Set(bundles).size !== bundles.length || bundles.some(n => !names.has(n))) fail('bundle', 'Bundle order must reference unique selected packages.');
|
|
55
|
+
const forms = new Set();
|
|
56
|
+
for (const s of array(value.settings ?? [], 'settings', 256)) {
|
|
57
|
+
closed(s, ['package', 'row', 'module', 'fields'], 'settings'); text(s.package, 'package'); text(s.row, 'row'); text(s.module, 'module');
|
|
58
|
+
const id = identity(s); if (forms.has(id)) fail('settings', 'Duplicate form.'); forms.add(id);
|
|
59
|
+
const seen = [];
|
|
60
|
+
for (const f of array(s.fields, 'fields', 2048)) {
|
|
61
|
+
closed(f, ['path', 'value'], 'field'); path(f.path);
|
|
62
|
+
if (!Object.hasOwn(f, 'value') || seen.some(p => overlaps(p, f.path))) fail('path', 'Missing value or overlapping fields.');
|
|
63
|
+
seen.push(f.path);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const rows = new Set();
|
|
67
|
+
for (const r of array(value.rows ?? [], 'rows', 512)) {
|
|
68
|
+
closed(r, ['package', 'row', 'module', 'enabled'], 'row');
|
|
69
|
+
text(r.package, 'package'); text(r.row, 'row'); text(r.module, 'module');
|
|
70
|
+
if (typeof r.enabled !== 'boolean' || !names.has(r.package) || rows.has(identity(r))) fail('row', 'Invalid row state.');
|
|
71
|
+
rows.add(identity(r));
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Authors may narrow an editable form; absence means include every public field. */
|
|
77
|
+
export function policy(value) {
|
|
78
|
+
if (value === undefined) return { exclude: [], include: null };
|
|
79
|
+
closed(value, ['share', 'include', 'exclude'], 'share policy');
|
|
80
|
+
if (value.share !== undefined && typeof value.share !== 'boolean') fail('policy', 'Invalid share policy.');
|
|
81
|
+
const list = key => array(value[key] ?? [], key).map(path);
|
|
82
|
+
return { exclude: list('exclude'), include: value.share === false ? [] : value.include === undefined ? null : list('include') };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Keep arrays atomic. If redaction touched an array, omit it instead of corrupting positions or secrets. */
|
|
86
|
+
export function publicFields(form, authorPolicy) {
|
|
87
|
+
const p = policy(authorPolicy), omitted = [], fields = [];
|
|
88
|
+
const blocked = [...p.exclude, ...(form.secrets ?? []).map(s => s.path)];
|
|
89
|
+
function visit(v, at) {
|
|
90
|
+
if (blocked.some(b => prefix(b, at))) { omitted.push({ path: at, reason: 'excluded-or-secret' }); return; }
|
|
91
|
+
if (p.include !== null && !p.include.some(b => overlaps(b, at))) return;
|
|
92
|
+
if (Array.isArray(v) && blocked.some(b => overlaps(b, at))) { omitted.push({ path: at, reason: 'array-contains-excluded-field' }); return; }
|
|
93
|
+
if (plain(v) && Object.keys(v).length) {
|
|
94
|
+
for (const [k, child] of Object.entries(v)) { if (!forbidden(k)) visit(child, [...at, k]); }
|
|
95
|
+
} else if (at.length && v !== undefined) {
|
|
96
|
+
if (p.include !== null && !p.include.some(b => prefix(b, at))) { omitted.push({ path: at, reason: 'partial-array-policy' }); return; }
|
|
97
|
+
fields.push({ path: at, value: v });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
visit(form.value, []); return { fields, omitted };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** An editable path must be volatile. Secret declarations in any schema branch win. */
|
|
104
|
+
export function schemaAllows(schema, at, proposed) {
|
|
105
|
+
function permission(node, parts, live = false, seen = new Set()) {
|
|
106
|
+
if (!node || seen.has(node)) return undefined;
|
|
107
|
+
if (node.meta?.role === 'secret') return false;
|
|
108
|
+
seen = new Set(seen).add(node); live ||= Boolean(node.meta?.volatile);
|
|
109
|
+
if (node.type === 'union' || node.type === 'intersect') {
|
|
110
|
+
const relevant = (node.list ?? []).map(n => permission(n, parts, live, seen)).filter(v => v !== undefined);
|
|
111
|
+
return relevant.length ? relevant.every(Boolean) : undefined;
|
|
112
|
+
}
|
|
113
|
+
if (node.type === 'transform') return permission(node.inner, parts, live, seen);
|
|
114
|
+
if (!parts.length) return live;
|
|
115
|
+
const [head, ...tail] = parts;
|
|
116
|
+
const next = node.type === 'object' ? node.dict?.[head] : ['dict', 'array'].includes(node.type) ? node.inner : undefined;
|
|
117
|
+
return permission(next, tail, live, seen);
|
|
118
|
+
}
|
|
119
|
+
if (permission(schema, at) !== true) return false;
|
|
120
|
+
if (Array.isArray(proposed) || plain(proposed)) {
|
|
121
|
+
return Object.entries(proposed).every(([key, value]) => schemaAllows(schema, [...at, key], value));
|
|
122
|
+
}
|
|
123
|
+
return true; // The native Settings service validates the complete Config and callbacks on write.
|
|
124
|
+
}
|
package/client.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: '@klarkxy/dsh-blueprint',
|
|
3
|
+
factory(require) {
|
|
4
|
+
const React = require('react'), h = React.createElement;
|
|
5
|
+
const PACKAGE = '@klarkxy/dsh-blueprint';
|
|
6
|
+
const idOf = f => JSON.stringify([f.package, f.row, f.module]);
|
|
7
|
+
const style = { border: '1px solid currentColor', borderRadius: 6, padding: '6px 10px', background: 'transparent', color: 'inherit', font: 'inherit' };
|
|
8
|
+
let focusPackage = null;
|
|
9
|
+
function Panel({ ctx }) {
|
|
10
|
+
const [zh, setZh] = React.useState(() => ctx.locale.getSnapshot().active.startsWith('zh'));
|
|
11
|
+
const t = (a, b) => zh ? a : b;
|
|
12
|
+
const [catalog, setCatalog] = React.useState(null), [selected, setSelected] = React.useState([]);
|
|
13
|
+
const [forms, setForms] = React.useState([]), [omit, setOmit] = React.useState([]);
|
|
14
|
+
const [mode, setMode] = React.useState('plugins'), [name, setName] = React.useState('My DSH setup');
|
|
15
|
+
const [generated, setGenerated] = React.useState(null), [source, setSource] = React.useState('');
|
|
16
|
+
const [preview, setPreview] = React.useState(null), [confirmed, setConfirmed] = React.useState(false);
|
|
17
|
+
const [report, setReport] = React.useState(null), [lastPlan, setLastPlan] = React.useState(null);
|
|
18
|
+
const [busy, setBusy] = React.useState(false), [error, setError] = React.useState('');
|
|
19
|
+
const lifetime = React.useRef(null);
|
|
20
|
+
async function rpc(endpoint, payload) {
|
|
21
|
+
const result = await ctx.connection.rpc.call('/dsh-blueprint', endpoint, payload, lifetime.current?.signal);
|
|
22
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
23
|
+
return result.value;
|
|
24
|
+
}
|
|
25
|
+
async function run(fn) {
|
|
26
|
+
setBusy(true); setError('');
|
|
27
|
+
try { await fn(); } catch (e) { setError(e instanceof Error ? e.message : 'Operation failed'); }
|
|
28
|
+
finally { setBusy(false); }
|
|
29
|
+
}
|
|
30
|
+
async function load() {
|
|
31
|
+
const data = await rpc('catalog', {}); setCatalog(data);
|
|
32
|
+
const selection = data.packages.filter(p => !p.readonly && !p.reason && (!focusPackage || p.name === focusPackage)).map(p => p.name);
|
|
33
|
+
setSelected(selection); setForms(data.forms.filter(f => f.builtin || selection.includes(f.package)).map(idOf));
|
|
34
|
+
setOmit([]); setGenerated(null); focusPackage = null;
|
|
35
|
+
}
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
lifetime.current = new AbortController();
|
|
38
|
+
const off = ctx.locale.subscribe(() => setZh(ctx.locale.getSnapshot().active.startsWith('zh')));
|
|
39
|
+
void run(load);
|
|
40
|
+
return () => { off(); lifetime.current.abort(); };
|
|
41
|
+
}, []);
|
|
42
|
+
const button = (label, action, disabled = false) => h('button', { type: 'button', style, disabled: busy || disabled, onClick: () => run(action) }, label);
|
|
43
|
+
const toggle = (list, value) => list.includes(value) ? list.filter(v => v !== value) : [...list, value];
|
|
44
|
+
const check = (label, checked, onChange, disabled = false) => h('label', { style: { display: 'flex', alignItems: 'start', gap: 8, padding: '5px 0', overflowWrap: 'anywhere' } },
|
|
45
|
+
h('input', { type: 'checkbox', checked, disabled: busy || disabled, onChange }), label);
|
|
46
|
+
function updateSource(value) { setSource(value); setPreview(null); setConfirmed(false); setReport(null); }
|
|
47
|
+
const code = value => h('pre', { style: { overflow: 'auto', maxHeight: 360, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere', padding: 12, border: '1px solid currentColor', borderRadius: 6 } }, JSON.stringify(value, null, 2));
|
|
48
|
+
return h('section', { 'aria-label': t('蓝图导入与导出', 'Blueprint import and export'), style: { display: 'grid', gap: 14, minWidth: 0 } },
|
|
49
|
+
h('p', null, t('分享插件组合和官方 Config/Settings 暴露的可编辑设置。不读取自定义页面、自建存储、会话或凭据库。', 'Share plugins and editable settings exposed by official Config/Settings. Custom pages, private storage, sessions and credential stores are not read.')),
|
|
50
|
+
error && h('p', { role: 'alert', style: { border: '1px solid currentColor', padding: 10 } }, error),
|
|
51
|
+
busy && h('p', { role: 'status' }, t('正在处理;安装进度也可在官方插件管理器中查看。', 'Working. Native package installation progress is also available in the official manager.')),
|
|
52
|
+
h('h3', null, t('导出蓝图', 'Export blueprint')),
|
|
53
|
+
button(t('刷新插件与原生设置', 'Refresh plugins and native settings'), load),
|
|
54
|
+
h('label', null, t('名称 ', 'Name '), h('input', { style, value: name, maxLength: 120, disabled: busy, onChange: e => { setName(e.target.value); setGenerated(null); } })),
|
|
55
|
+
h('label', null, t('分享范围 ', 'Share '), h('select', { style, value: mode, disabled: busy, onChange: e => { setMode(e.target.value); setGenerated(null); } },
|
|
56
|
+
h('option', { value: 'plugins' }, t('仅插件组合和顺序', 'Plugins and order only')),
|
|
57
|
+
h('option', { value: 'settings' }, t('插件组合及设置', 'Plugins and settings')))),
|
|
58
|
+
catalog && h('div', null, catalog.packages.map(p => h('div', { key: p.name }, check(
|
|
59
|
+
`${p.name}@${p.version ?? '?'}${p.reason ? ` (${p.reason})` : ''}${p.readonly ? t('(宿主管理,不导出)', ' (host-managed)') : ''}`,
|
|
60
|
+
selected.includes(p.name), () => { setSelected(toggle(selected, p.name)); setGenerated(null); }, p.readonly || Boolean(p.reason))))),
|
|
61
|
+
mode === 'settings' && catalog && h('div', null,
|
|
62
|
+
h('p', null, t('以官方可编辑字段为准,不推断自定义页面的可见字段。普通字段默认包含;可以取消任一表单或字段。请检查未标记的私密内容。', 'The official editable fields define scope, not custom-page visibility. Ordinary fields are included by default. Uncheck any form or field and review unmarked private text.')),
|
|
63
|
+
catalog.forms.filter(f => f.builtin || selected.includes(f.package)).map(f => h('details', { key: idOf(f) },
|
|
64
|
+
h('summary', null, `${f.row} · ${f.package}`),
|
|
65
|
+
check(t('包含此原生配置', 'Include these native settings'), forms.includes(idOf(f)), () => { setForms(toggle(forms, idOf(f))); setGenerated(null); }),
|
|
66
|
+
f.fields.map(field => {
|
|
67
|
+
const key = JSON.stringify([idOf(f), field.path]);
|
|
68
|
+
return h('div', { key }, check(`${field.path.join('.')} = ${JSON.stringify(field.value)}`, !omit.includes(key), () => { setOmit(toggle(omit, key)); setGenerated(null); }, !forms.includes(idOf(f))));
|
|
69
|
+
}), f.omitted.length > 0 && code(f.omitted))),
|
|
70
|
+
),
|
|
71
|
+
catalog?.warnings?.length > 0 && h('details', null, h('summary', null, t('未导出的项目与限制', 'Unavailable configurations and limits')), code(catalog.warnings)),
|
|
72
|
+
button(t('生成并预览', 'Generate and review'), async () => setGenerated(await rpc('generate', {
|
|
73
|
+
mode, name, packages: selected,
|
|
74
|
+
forms: forms.filter(id => catalog.forms.some(f => idOf(f) === id && (f.builtin || selected.includes(f.package)))),
|
|
75
|
+
omit: omit.map(key => { const [form, path] = JSON.parse(key); return { form, path }; }),
|
|
76
|
+
})), !catalog || !name.trim()),
|
|
77
|
+
generated && h('div', null, code(generated.blueprint),
|
|
78
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
79
|
+
button(t('复制分享码', 'Copy share code'), () => navigator.clipboard.writeText(generated.code)),
|
|
80
|
+
button(t('保存 JSON', 'Save JSON'), async () => {
|
|
81
|
+
const url = URL.createObjectURL(new Blob([generated.json], { type: 'application/json' }));
|
|
82
|
+
const a = document.createElement('a'); a.href = url; a.download = 'setup.dsh-blueprint.json'; a.click();
|
|
83
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
84
|
+
})),
|
|
85
|
+
h('details', null, h('summary', null, t('分享码(也可手动复制)', 'Share code (manual copy)')), h('textarea', { readOnly: true, value: generated.code, rows: 4, style: { ...style, width: '100%', boxSizing: 'border-box' } }))),
|
|
86
|
+
h('hr'), h('h3', null, t('导入到当前 profile', 'Import into the current profile')),
|
|
87
|
+
h('p', null, t('不会新建空间或删除现有插件。蓝图中的插件会保持相对顺序;需要重排时放在未选插件之后。', 'No spaces are created and no existing packages are removed. Imported bundles retain their relative order; reordered bundles follow unselected bundles.')),
|
|
88
|
+
h('label', null, t('读取蓝图文件 ', 'Read blueprint file '), h('input', { type: 'file', accept: '.json,.txt', disabled: busy, onChange: e => { const file = e.target.files?.[0]; if (file) void run(async () => {
|
|
89
|
+
if (file.size > 2 * 1024 * 1024) throw new Error('File exceeds 2 MiB');
|
|
90
|
+
updateSource(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await file.arrayBuffer()));
|
|
91
|
+
}); } })),
|
|
92
|
+
h('textarea', { 'aria-label': t('JSON 或分享码', 'JSON or share code'), value: source, rows: 7, disabled: busy,
|
|
93
|
+
maxLength: 2 * 1024 * 1024, style: { ...style, width: '100%', boxSizing: 'border-box' }, onChange: e => updateSource(e.target.value) }),
|
|
94
|
+
button(t('预览导入变更', 'Preview import changes'), async () => { setConfirmed(false); setPreview(await rpc('preview', { text: source })); }, !source.trim()),
|
|
95
|
+
preview && h('div', null,
|
|
96
|
+
preview.blockers.length > 0 && h('div', { role: 'alert' }, code(preview.blockers)),
|
|
97
|
+
code({ stage: preview.stage, operations: preview.operations, finalOrder: preview.order }),
|
|
98
|
+
preview.next && h('p', null, t('这一步仅安装/启用插件。完成后再次预览,核对真实配置字段后再确认写入设置。', 'This stage only installs/enables plugins. Preview again afterwards to review the live configuration fields before writing settings.')),
|
|
99
|
+
check(t('我已核对变更,并信任这些插件代码。安装可能运行先前已授权的构建脚本;启用会执行插件。', 'I reviewed the changes and trust the plugin code. Installation may run previously approved build scripts; enabling executes plugins.'), confirmed, () => setConfirmed(!confirmed)),
|
|
100
|
+
button(t('确认执行本次变更', 'Apply these changes'), async () => {
|
|
101
|
+
const id = preview.planId; setLastPlan(id);
|
|
102
|
+
const result = await rpc('apply', { planId: id, confirmed: true }); setReport(result); setPreview(null); setConfirmed(false);
|
|
103
|
+
}, !preview.planId || !confirmed)),
|
|
104
|
+
report && h('div', { role: 'status' }, h('strong', null, t('执行结果', 'Execution result')), code(report),
|
|
105
|
+
report.next && h('p', null, t('请再次点击“预览导入变更”以继续设置阶段。', 'Select Preview import changes again to continue with settings.'))),
|
|
106
|
+
lastPlan && button(t('读取该次执行结果(不重试执行)', 'Read this execution result (no replay)'), async () => {
|
|
107
|
+
const result = await rpc('result', { planId: lastPlan });
|
|
108
|
+
if (!result) throw new Error(t('结果尚未生成或服务已重启;请检查官方管理器,不要假定操作未发生。', 'No retained result yet, or the service restarted. Check the official manager; do not assume nothing changed.'));
|
|
109
|
+
setReport(result);
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
inject: ['slots', 'locale', 'connection', 'pluginNavigation'],
|
|
115
|
+
apply(ctx) {
|
|
116
|
+
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({ name: 'plugins.bundle.config', key: PACKAGE }, () => h(Panel, { ctx })));
|
|
117
|
+
ctx.slots.inject('plugins.detail.actions', () => ctx.slots.register({ name: 'plugins.detail.actions', id: 'klarkxy-blueprint-share', order: 40 }, ({ subject }) => {
|
|
118
|
+
if (subject.kind !== 'bundle' || subject.pkg.name === PACKAGE) return null;
|
|
119
|
+
return h('button', { type: 'button', style, onClick: () => { focusPackage = subject.pkg.name; ctx.pluginNavigation.openBundle(PACKAGE); } }, ctx.locale.getSnapshot().active.startsWith('zh') ? '分享蓝图' : 'Share blueprint');
|
|
120
|
+
}));
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
package/codec.mjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Adapted from dsh-spaces blueprint-codec.ts (MIT); see NOTICE.md.
|
|
2
|
+
import { deflateRawSync, inflateRawSync } from 'node:zlib';
|
|
3
|
+
export const MAX_JSON = 1024 * 1024;
|
|
4
|
+
export const MAX_SHARE = 2 * MAX_JSON;
|
|
5
|
+
export class BlueprintError extends Error {
|
|
6
|
+
constructor(code, message) { super(message); this.name = 'BlueprintError'; this.code = code; }
|
|
7
|
+
}
|
|
8
|
+
export function fail(code, message) { throw new BlueprintError(code, message); }
|
|
9
|
+
export const plain = value => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
&& [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
11
|
+
export const forbidden = key => ['__proto__', 'prototype', 'constructor'].includes(key);
|
|
12
|
+
|
|
13
|
+
/** Strict JSON with duplicate-member, depth, Unicode and numeric checks. Never evaluates code. */
|
|
14
|
+
export function parseJson(text) {
|
|
15
|
+
if (typeof text !== 'string') fail('json', 'Expected JSON text.');
|
|
16
|
+
if (Buffer.byteLength(text) > MAX_JSON) fail('size', 'JSON exceeds 1 MiB.');
|
|
17
|
+
let i = 0;
|
|
18
|
+
const error = () => fail('json', `Invalid JSON near offset ${i}.`);
|
|
19
|
+
const ws = () => { while (/[ \t\r\n]/.test(text[i] ?? '\0')) i++; };
|
|
20
|
+
function str() {
|
|
21
|
+
const start = i++;
|
|
22
|
+
while (i < text.length) {
|
|
23
|
+
const c = text[i++];
|
|
24
|
+
if (c === '\\') { i++; continue; }
|
|
25
|
+
if (c === '"') {
|
|
26
|
+
let s; try { s = JSON.parse(text.slice(start, i)); } catch { error(); }
|
|
27
|
+
for (let n = 0; n < s.length; n++) {
|
|
28
|
+
const u = s.charCodeAt(n);
|
|
29
|
+
if (u >= 0xd800 && u <= 0xdbff) {
|
|
30
|
+
const v = s.charCodeAt(++n);
|
|
31
|
+
if (!(v >= 0xdc00 && v <= 0xdfff)) error();
|
|
32
|
+
} else if (u >= 0xdc00 && u <= 0xdfff) error();
|
|
33
|
+
}
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
error();
|
|
38
|
+
}
|
|
39
|
+
function value(depth) {
|
|
40
|
+
ws();
|
|
41
|
+
if (depth > 64 && (text[i] === '{' || text[i] === '[')) fail('depth', 'JSON exceeds 64 container levels.');
|
|
42
|
+
const c = text[i];
|
|
43
|
+
if (c === '"') return str();
|
|
44
|
+
if (c === '{' || c === '[') {
|
|
45
|
+
const array = c === '[', out = array ? [] : {}, keys = new Set();
|
|
46
|
+
const end = array ? ']' : '}'; i++; ws();
|
|
47
|
+
if (text[i] === end) { i++; return out; }
|
|
48
|
+
while (i < text.length) {
|
|
49
|
+
let key;
|
|
50
|
+
if (!array) {
|
|
51
|
+
ws(); if (text[i] !== '"') error(); key = str();
|
|
52
|
+
if (keys.has(key)) fail('duplicate', 'Duplicate JSON object member.');
|
|
53
|
+
if (forbidden(key)) fail('key', 'Unsafe object member.');
|
|
54
|
+
keys.add(key); ws(); if (text[i++] !== ':') error();
|
|
55
|
+
}
|
|
56
|
+
const child = value(depth + 1);
|
|
57
|
+
if (array) out.push(child); else out[key] = child;
|
|
58
|
+
ws(); if (text[i] === end) { i++; return out; }
|
|
59
|
+
if (text[i++] !== ',') error();
|
|
60
|
+
}
|
|
61
|
+
error();
|
|
62
|
+
}
|
|
63
|
+
for (const [token, v] of [['true', true], ['false', false], ['null', null]]) {
|
|
64
|
+
if (text.startsWith(token, i)) { i += token.length; return v; }
|
|
65
|
+
}
|
|
66
|
+
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(text.slice(i));
|
|
67
|
+
if (!match) error(); i += match[0].length;
|
|
68
|
+
const n = Number(match[0]);
|
|
69
|
+
if (!Number.isFinite(n) || (Number.isInteger(n) && !Number.isSafeInteger(n))) fail('number', 'Unsafe JSON number.');
|
|
70
|
+
return Object.is(n, -0) ? 0 : n;
|
|
71
|
+
}
|
|
72
|
+
const out = value(1); ws(); if (i !== text.length) error(); return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function decode(text) {
|
|
76
|
+
if (typeof text !== 'string' || Buffer.byteLength(text) > MAX_SHARE) fail('size', 'Input exceeds 2 MiB.');
|
|
77
|
+
text = text.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
|
|
78
|
+
if (text.startsWith('DSHBP1:')) fail('legacy', 'Spaces v1 creates spaces. It is not a current-profile import; export v2 from the plugin manager.');
|
|
79
|
+
if (!text.startsWith('DSHBP')) return parseJson(text);
|
|
80
|
+
const m = /^DSHBP2:([JZ]):([A-Za-z0-9_-]+)$/.exec(text);
|
|
81
|
+
if (!m) fail('encoding', 'Unsupported blueprint code version or encoding.');
|
|
82
|
+
let bytes = Buffer.from(m[2], 'base64url');
|
|
83
|
+
if (bytes.toString('base64url') !== m[2]) fail('base64', 'Noncanonical Base64url.');
|
|
84
|
+
if (m[1] === 'Z') {
|
|
85
|
+
let result;
|
|
86
|
+
try { result = inflateRawSync(bytes, { maxOutputLength: MAX_JSON, info: true }); }
|
|
87
|
+
catch { fail('deflate', 'Invalid or oversized raw DEFLATE payload.'); }
|
|
88
|
+
if (result.engine.bytesWritten !== bytes.length) fail('deflate', 'Trailing DEFLATE data.');
|
|
89
|
+
bytes = result.buffer;
|
|
90
|
+
}
|
|
91
|
+
if (bytes.length > MAX_JSON) fail('size', 'Decoded JSON exceeds 1 MiB.');
|
|
92
|
+
let json; try { json = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
93
|
+
catch { fail('utf8', 'Invalid UTF-8.'); }
|
|
94
|
+
return parseJson(json);
|
|
95
|
+
}
|
|
96
|
+
export function encode(value, mode = 'shortest') {
|
|
97
|
+
if (!['J', 'Z', 'shortest'].includes(mode)) fail('encoding', 'Unsupported encoding.');
|
|
98
|
+
const json = JSON.stringify(value); parseJson(json);
|
|
99
|
+
const bytes = Buffer.from(json), j = `DSHBP2:J:${bytes.toString('base64url')}`;
|
|
100
|
+
if (mode === 'J') return j;
|
|
101
|
+
const z = `DSHBP2:Z:${deflateRawSync(bytes).toString('base64url')}`;
|
|
102
|
+
return mode === 'Z' || z.length < j.length ? z : j;
|
|
103
|
+
}
|
package/cordis.patch.yml
ADDED
package/engine.mjs
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { decode, encode, fail } from './codec.mjs';
|
|
3
|
+
import { validate, closed, array, text, path, identity, equal, overlaps, prefix, publicFields, policy, get } from './blueprint.mjs';
|
|
4
|
+
|
|
5
|
+
function changesConnection(at, before, after) {
|
|
6
|
+
if (equal(before, after)) return false;
|
|
7
|
+
if (at.some(p => /^(?:base[_-]?url|endpoint|url|host|api[_-]?key[_-]?env)$/i.test(p))) return true;
|
|
8
|
+
const keys = value => value !== null && typeof value === 'object' ? Object.keys(value) : [];
|
|
9
|
+
// Whole-container edits must also protect nested additions, removals and replacements.
|
|
10
|
+
return [...new Set([...keys(before), ...keys(after)])]
|
|
11
|
+
.some(key => changesConnection([...at, key], get(before, [key]), get(after, [key])));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class BlueprintEngine {
|
|
15
|
+
constructor(port, now = Date.now) { this.port = port; this.now = now; this.plans = new Map(); this.results = new Map(); this.busy = false; }
|
|
16
|
+
fields(form) {
|
|
17
|
+
const result = publicFields(form, form.policy), fields = [];
|
|
18
|
+
for (const field of result.fields) {
|
|
19
|
+
if (this.port.allows(form, field.path, field.value)) fields.push(field);
|
|
20
|
+
else result.omitted.push({ path: field.path, reason: 'not-native-editable' });
|
|
21
|
+
}
|
|
22
|
+
return { fields, omitted: result.omitted };
|
|
23
|
+
}
|
|
24
|
+
async catalog() {
|
|
25
|
+
const s = await this.port.snapshot();
|
|
26
|
+
return {
|
|
27
|
+
packages: s.packages.map(({ name, version, source, enabled, readonly, reason }) => ({ name, version, source, enabled, readonly, reason })),
|
|
28
|
+
forms: s.forms.map(f => ({ package: f.package, row: f.row, module: f.module, builtin: f.builtin, ...this.fields(f) })),
|
|
29
|
+
warnings: s.warnings,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async generate(request) {
|
|
33
|
+
closed(request, ['packages', 'forms', 'omit', 'mode', 'name'], 'export request');
|
|
34
|
+
if (!['plugins', 'settings'].includes(request.mode)) fail('mode', 'Choose plugins or settings.');
|
|
35
|
+
const selected = new Set(array(request.packages, 'selected packages', 128).map(n => text(n, 'package')));
|
|
36
|
+
const selectedForms = new Set(array(request.forms ?? [], 'selected forms').map(n => text(n, 'form', 2000)));
|
|
37
|
+
const omit = array(request.omit ?? [], 'omitted fields', 2048);
|
|
38
|
+
for (const o of omit) { closed(o, ['form', 'path'], 'omitted field'); text(o.form, 'form', 2000); path(o.path); }
|
|
39
|
+
const s = await this.port.snapshot(), packages = [];
|
|
40
|
+
for (const name of selected) {
|
|
41
|
+
const p = s.packages.find(p => p.name === name);
|
|
42
|
+
if (!p || p.readonly || !p.source || p.reason) fail('package', `Cannot export ${name}; inspect its status in the plugin manager.`);
|
|
43
|
+
packages.push({ name, version: p.version, source: p.source });
|
|
44
|
+
}
|
|
45
|
+
// Native order, not checkbox order or alphabetical UI card order.
|
|
46
|
+
packages.sort((a, b) => s.packages.findIndex(p => p.name === a.name) - s.packages.findIndex(p => p.name === b.name));
|
|
47
|
+
const blueprint = {
|
|
48
|
+
kind: 'dsh-blueprint', formatVersion: 2, metadata: { name: text(request.name, 'name', 120), version: '1.0.0' },
|
|
49
|
+
packages, bundles: s.order.filter(n => selected.has(n)),
|
|
50
|
+
};
|
|
51
|
+
const omitted = [];
|
|
52
|
+
if (request.mode === 'settings') {
|
|
53
|
+
blueprint.settings = [];
|
|
54
|
+
for (const id of selectedForms) {
|
|
55
|
+
const f = s.forms.find(f => identity(f) === id);
|
|
56
|
+
if (!f || (!selected.has(f.package) && !f.builtin)) fail('form', 'Selected form is not available for the selected packages.');
|
|
57
|
+
const result = this.fields(f);
|
|
58
|
+
const fields = result.fields.filter(field => !omit.some(o => o.form === id && overlaps(o.path, field.path)));
|
|
59
|
+
omitted.push(...result.omitted.map(o => ({ form: id, ...o })));
|
|
60
|
+
if (fields.length) blueprint.settings.push({ package: f.package, row: f.row, module: f.module, fields });
|
|
61
|
+
}
|
|
62
|
+
blueprint.rows = s.rows.filter(r => selected.has(r.package) && !r.readonly)
|
|
63
|
+
.map(({ package: pkg, row, module, enabled }) => ({ package: pkg, row, module, enabled }));
|
|
64
|
+
}
|
|
65
|
+
const checked = validate(blueprint);
|
|
66
|
+
return { blueprint: checked, json: JSON.stringify(checked, null, 2), code: encode(checked), omitted, warnings: s.warnings };
|
|
67
|
+
}
|
|
68
|
+
async preview(request) {
|
|
69
|
+
closed(request, ['text'], 'preview request');
|
|
70
|
+
const bp = validate(decode(request.text));
|
|
71
|
+
const s = await this.port.snapshot(), operations = [], blockers = [];
|
|
72
|
+
const selected = new Set(bp.packages.map(p => p.name));
|
|
73
|
+
for (const p of bp.packages) {
|
|
74
|
+
const current = s.packages.find(x => x.name === p.name);
|
|
75
|
+
if (current) {
|
|
76
|
+
if (current.readonly || current.reason || current.source !== p.source || current.version !== p.version) blockers.push(`${p.name}: protected, unsupported source or different version; resolve it in the official manager first.`);
|
|
77
|
+
} else if (p.source !== 'npm') blockers.push(`${p.name}: this host does not supply the requested built-in bundle.`);
|
|
78
|
+
else {
|
|
79
|
+
const inspected = await this.port.inspect(`${p.name}@${p.version}`);
|
|
80
|
+
if (inspected.status !== 'accepted' || inspected.bundle !== true || inspected.name !== p.name || inspected.version !== p.version) blockers.push(`${p.name}: the official manager did not confirm this exact bundle.`);
|
|
81
|
+
else operations.push({ type: 'install', name: p.name, version: p.version });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const currentOrder = s.order.filter(n => selected.has(n));
|
|
85
|
+
const reorder = !equal(currentOrder, bp.bundles);
|
|
86
|
+
if (reorder) {
|
|
87
|
+
for (const name of currentOrder) operations.push({ type: 'bundle', name, enabled: false });
|
|
88
|
+
for (const name of bp.bundles) operations.push({ type: 'bundle', name, enabled: true });
|
|
89
|
+
}
|
|
90
|
+
const packageStage = operations.length > 0;
|
|
91
|
+
const settingsRequested = (bp.settings?.length ?? 0) + (bp.rows?.length ?? 0) > 0;
|
|
92
|
+
if (!packageStage) {
|
|
93
|
+
for (const config of bp.settings ?? []) {
|
|
94
|
+
const form = s.forms.find(f => identity(f) === identity(config));
|
|
95
|
+
if (!form || (!selected.has(form.package) && !form.builtin)) { blockers.push(`${config.row}: no matching native editable Settings form on this host; custom configuration is not imported.`); continue; }
|
|
96
|
+
const rules = policy(form.policy), edits = [];
|
|
97
|
+
for (const field of config.fields) {
|
|
98
|
+
const denied = (form.secrets ?? []).some(secret => overlaps(secret.path, field.path))
|
|
99
|
+
|| rules.exclude.some(p => overlaps(p, field.path))
|
|
100
|
+
|| rules.include !== null && !rules.include.some(p => prefix(p, field.path));
|
|
101
|
+
if (denied || !this.port.allows(form, field.path, field.value)) { blockers.push(`${config.row}/${field.path.join('/')}: not a shareable editable field.`); continue; }
|
|
102
|
+
if (equal(get(form.value, field.path), field.value)) continue;
|
|
103
|
+
if (changesConnection(field.path, get(form.value, field.path), field.value)) {
|
|
104
|
+
blockers.push(`${config.row}: changing a connection target while retaining a credential requires rebinding in the official configuration page.`); continue;
|
|
105
|
+
}
|
|
106
|
+
edits.push({ op: 'set', path: field.path, value: field.value });
|
|
107
|
+
}
|
|
108
|
+
if (edits.length) operations.push({ type: 'settings', package: config.package, row: config.row, module: config.module, revision: form.revision, edits });
|
|
109
|
+
}
|
|
110
|
+
for (const row of bp.rows ?? []) {
|
|
111
|
+
const current = s.rows.find(r => identity(r) === identity(row));
|
|
112
|
+
if (!current || current.readonly) { blockers.push(`${row.row}: unavailable or protected row.`); continue; }
|
|
113
|
+
if (current.enabled !== row.enabled) operations.push({ type: 'row', entryId: current.entryId, row: row.row, enabled: row.enabled });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Inspection can involve network I/O. Bind the plan only if the observed profile stayed unchanged.
|
|
117
|
+
if ((await this.port.snapshot()).stamp !== s.stamp) fail('stale', 'Profile changed while preparing the preview.');
|
|
118
|
+
const result = {
|
|
119
|
+
stage: packageStage ? 'packages' : 'settings', blockers,
|
|
120
|
+
operations, next: packageStage && settingsRequested ? 'preview-settings' : null,
|
|
121
|
+
order: reorder ? [...s.order.filter(n => !selected.has(n)), ...bp.bundles] : s.order,
|
|
122
|
+
notice: 'No packages are removed. Native installation may execute previously approved package scripts. Enabling bundles executes plugin code. Settings are additive field edits, not a full environment clone.',
|
|
123
|
+
};
|
|
124
|
+
if (blockers.length) return { ...result, planId: null };
|
|
125
|
+
for (const [id, p] of this.plans) if (p.expires <= this.now()) this.plans.delete(id);
|
|
126
|
+
if (this.plans.size >= 8) fail('capacity', 'Too many outstanding previews. Let an old preview expire.');
|
|
127
|
+
const planId = randomUUID();
|
|
128
|
+
this.plans.set(planId, { ...result, stamp: s.stamp, expires: this.now() + 300_000 });
|
|
129
|
+
return { ...result, planId };
|
|
130
|
+
}
|
|
131
|
+
async apply(planId, signal = new AbortController().signal) {
|
|
132
|
+
text(planId, 'plan id');
|
|
133
|
+
if (this.results.has(planId)) return this.results.get(planId);
|
|
134
|
+
if (this.busy) fail('busy', 'A blueprint operation is already running.');
|
|
135
|
+
const p = this.plans.get(planId);
|
|
136
|
+
if (!p || p.expires <= this.now()) { this.plans.delete(planId); fail('expired', 'Preview expired or was already consumed.'); }
|
|
137
|
+
this.busy = true;
|
|
138
|
+
const report = { planId, status: 'applied', next: p.next, steps: [] };
|
|
139
|
+
try {
|
|
140
|
+
if ((await this.port.snapshot()).stamp !== p.stamp) fail('stale', 'Profile changed after preview.');
|
|
141
|
+
this.plans.delete(planId);
|
|
142
|
+
let stamp = p.stamp;
|
|
143
|
+
for (const operation of p.operations) {
|
|
144
|
+
if (signal.aborted) { report.status = 'interrupted'; break; }
|
|
145
|
+
try {
|
|
146
|
+
const before = await this.port.snapshot();
|
|
147
|
+
if (before.stamp !== stamp) { report.status = 'conflict'; break; }
|
|
148
|
+
const outcome = await this.port.execute(operation, signal);
|
|
149
|
+
report.steps.push({ type: operation.type, target: operation.name ?? operation.row, outcome });
|
|
150
|
+
if (outcome.application && outcome.application !== 'applied') { report.status = outcome.application; break; }
|
|
151
|
+
const after = await this.port.snapshot();
|
|
152
|
+
if (!this.port.verify(operation, after)) { report.status = 'verification-failed'; break; }
|
|
153
|
+
stamp = after.stamp;
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Plugin validation errors can contain submitted values or credentials. Never echo arbitrary messages.
|
|
156
|
+
if (report.steps.length === p.operations.indexOf(operation)) report.steps.push({ type: operation.type, target: operation.name ?? operation.row, outcome: { application: 'failed', code: 'operation-failed' } });
|
|
157
|
+
report.status = 'failed'; break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (report.status === 'applied') {
|
|
161
|
+
try { if (!equal((await this.port.snapshot()).order, p.order)) report.status = 'verification-failed'; }
|
|
162
|
+
catch { report.status = 'verification-failed'; }
|
|
163
|
+
}
|
|
164
|
+
report.remaining = p.operations.length - report.steps.length;
|
|
165
|
+
if (report.status !== 'applied') report.next = null;
|
|
166
|
+
this.results.set(planId, report);
|
|
167
|
+
if (this.results.size > 8) this.results.delete(this.results.keys().next().value);
|
|
168
|
+
return report;
|
|
169
|
+
} finally { this.busy = false; }
|
|
170
|
+
}
|
|
171
|
+
dispose() { this.plans.clear(); this.results.clear(); }
|
|
172
|
+
}
|
package/index.mjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BlueprintError, MAX_SHARE } from './codec.mjs';
|
|
2
|
+
import { closed, text } from './blueprint.mjs';
|
|
3
|
+
import { BlueprintEngine } from './engine.mjs';
|
|
4
|
+
import { officialPort } from './official.mjs';
|
|
5
|
+
export const name = 'dsh-blueprint';
|
|
6
|
+
export const inject = ['connection', 'pluginManager', 'profileContext', 'pluginPackages', 'settings', 'loader'];
|
|
7
|
+
export function apply(ctx) {
|
|
8
|
+
const engine = new BlueprintEngine(officialPort(ctx)), lifetime = new AbortController();
|
|
9
|
+
ctx.effect(() => () => { lifetime.abort(); engine.dispose(); });
|
|
10
|
+
// The official Connection registry supplies authentication and the Host/Origin fence.
|
|
11
|
+
ctx.connection.rpc.handle('/dsh-blueprint', async (endpoint, payload, signal) => {
|
|
12
|
+
try {
|
|
13
|
+
if (Buffer.byteLength(JSON.stringify(payload) ?? '') > MAX_SHARE + 65536) throw new BlueprintError('size', 'Request too large.');
|
|
14
|
+
const combined = AbortSignal.any([signal, lifetime.signal]); combined.throwIfAborted();
|
|
15
|
+
let value;
|
|
16
|
+
switch (endpoint) {
|
|
17
|
+
case 'catalog': closed(payload, [], 'catalog request'); value = await engine.catalog(); break;
|
|
18
|
+
case 'generate': value = await engine.generate(payload); break;
|
|
19
|
+
case 'preview': value = await engine.preview(payload); break;
|
|
20
|
+
case 'apply':
|
|
21
|
+
closed(payload, ['planId', 'confirmed'], 'apply request');
|
|
22
|
+
if (payload.confirmed !== true) throw new BlueprintError('confirmation', 'Explicit confirmation is required.');
|
|
23
|
+
value = await engine.apply(text(payload.planId, 'plan id'), combined); break;
|
|
24
|
+
case 'result': closed(payload, ['planId'], 'result request'); value = engine.results.get(text(payload.planId, 'plan id')) ?? null; break;
|
|
25
|
+
default: throw new BlueprintError('endpoint', 'Unknown blueprint endpoint.');
|
|
26
|
+
}
|
|
27
|
+
return { ok: true, value };
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return { ok: false, error: { code: e instanceof BlueprintError ? e.code : 'operation-failed',
|
|
30
|
+
message: e instanceof BlueprintError ? e.message : 'The operation failed. Review the official plugin manager; no automatic retry was performed.', details: {} } };
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"name":"Blueprints","description":"Share and import plugin combinations and configuration presets in the official plugin manager."}}
|
package/locale/zh.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"name":"蓝图","description":"在官方插件管理器内分享、导入插件组合与配置预设。"}}
|
package/official.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** Official DSH adapter. No private Spaces APIs, filesystem writes, shell commands or credential reads. */
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { PACKAGE, closed, exactVersion, identity, equal, get, policy, schemaAllows } from './blueprint.mjs';
|
|
7
|
+
import { parseJson, plain, fail } from './codec.mjs';
|
|
8
|
+
// Native Settings descriptors are the data contract. UI slots and custom
|
|
9
|
+
// pages are presentation only: never inspect them to infer a storage schema.
|
|
10
|
+
function cleanResult(r) {
|
|
11
|
+
if (!r || !['applied', 'restart-required', 'overridden', 'failed', 'cancelled'].includes(r.application)) fail('native-contract', 'Unsupported native management result.');
|
|
12
|
+
return { application: r.application, changed: r.changed,
|
|
13
|
+
...(r.error ? { code: r.error.code } : {}),
|
|
14
|
+
...(r.pendingBuilds?.length ? { pendingBuilds: r.pendingBuilds } : {}) };
|
|
15
|
+
}
|
|
16
|
+
export function officialPort(ctx) {
|
|
17
|
+
const manager = ctx.pluginManager;
|
|
18
|
+
for (const name of ['listBundles', 'listPlugins', 'inspect', 'installBundle', 'setBundleEnabled', 'setPluginEnabled']) {
|
|
19
|
+
if (typeof manager?.[name] !== 'function') fail('capability', `Official pluginManager.${name} is required.`);
|
|
20
|
+
}
|
|
21
|
+
if (typeof ctx.settings?.describe !== 'function' || typeof ctx.settings?.mutate !== 'function'
|
|
22
|
+
|| typeof ctx.loader?.entries !== 'function' || !ctx.profileContext?.dir) fail('capability', 'Official profile-backed Settings and Loader services are required.');
|
|
23
|
+
const manifestPath = join(ctx.profileContext.dir, 'package.json');
|
|
24
|
+
async function snapshot() {
|
|
25
|
+
const before = readFileSync(manifestPath, 'utf8');
|
|
26
|
+
const manifest = parseJson(before);
|
|
27
|
+
const [nativeBundles, nativeRows] = await Promise.all([manager.listBundles(), manager.listPlugins()]);
|
|
28
|
+
const descriptors = ctx.settings.describe({ redactSecrets: true });
|
|
29
|
+
if (!Array.isArray(descriptors) || descriptors.some(d => d.applies !== 'live' || !Array.isArray(d.secrets))) fail('capability', 'This host lacks the redacted, profile-backed Settings form contract.');
|
|
30
|
+
const packages = nativeBundles.map(b => {
|
|
31
|
+
const readonly = Boolean(b.readOnlyReason || b.name === PACKAGE || b.rows.some(r => r.moduleName === PACKAGE)
|
|
32
|
+
|| !b.installed && !b.optional);
|
|
33
|
+
const spec = manifest.dependencies?.[b.name];
|
|
34
|
+
// Only registry dependencies can honestly be exported as npm identities. Local/git/tarball sources stay unsupported.
|
|
35
|
+
const registry = typeof spec === 'string' && /^[0-9A-Za-z*~^<>=|.+ -]+$/.test(spec);
|
|
36
|
+
const source = b.installed ? registry ? 'npm' : null : 'builtin';
|
|
37
|
+
const reason = b.error ? 'native-bundle-error' : !exactVersion(b.version) ? 'unknown-exact-version' : !source ? 'non-registry-source' : null;
|
|
38
|
+
return { name: b.name, version: b.version ?? null, source, enabled: b.enabled, readonly, reason };
|
|
39
|
+
});
|
|
40
|
+
const order = manifest.dsh?.profile?.bundles;
|
|
41
|
+
if (!Array.isArray(order) || order.some(n => typeof n !== 'string')) fail('profile', 'No native ordered bundle manifest.');
|
|
42
|
+
const rows = [], forms = [], warnings = [];
|
|
43
|
+
const entries = [...ctx.loader.entries()];
|
|
44
|
+
const policies = new Map();
|
|
45
|
+
for (const b of nativeBundles) {
|
|
46
|
+
let declarations = {};
|
|
47
|
+
try {
|
|
48
|
+
const dir = ctx.pluginPackages?.packageOf(b.name, pathToFileURL(manifestPath).href)?.dir;
|
|
49
|
+
if (!dir) throw new Error('unresolved');
|
|
50
|
+
const meta = parseJson(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
51
|
+
if (meta.name !== b.name || meta.version !== b.version) throw new Error('identity');
|
|
52
|
+
if (meta.dshBlueprint !== undefined) {
|
|
53
|
+
closed(meta.dshBlueprint, ['version', 'entries'], 'blueprint contribution');
|
|
54
|
+
if (meta.dshBlueprint.version !== 1 || !plain(meta.dshBlueprint.entries)) throw new Error('policy');
|
|
55
|
+
declarations = meta.dshBlueprint.entries;
|
|
56
|
+
for (const v of Object.values(declarations)) policy(v);
|
|
57
|
+
}
|
|
58
|
+
policies.set(b.name, declarations);
|
|
59
|
+
} catch { warnings.push(`${b.name}: configuration policy could not be read safely; only the package list is available.`); }
|
|
60
|
+
for (const row of b.rows) {
|
|
61
|
+
const native = nativeRows.find(r => r.entryId === row.entryId);
|
|
62
|
+
if (native) rows.push({ package: b.name, row: row.rowId, module: row.moduleName, entryId: native.entryId,
|
|
63
|
+
enabled: native.enabled, readonly: Boolean(native.readOnlyReason || b.name === PACKAGE) });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const d of descriptors) {
|
|
67
|
+
const owners = nativeBundles.flatMap(b => b.rows.filter(r => r.rowId === d.ns).map(r => ({ b, r })));
|
|
68
|
+
if (owners.length !== 1) { warnings.push(`${d.ns}: native configuration ownership is ambiguous or unavailable.`); continue; }
|
|
69
|
+
const { b, r } = owners[0];
|
|
70
|
+
if (b.name === PACKAGE || !policies.has(b.name)) continue;
|
|
71
|
+
const native = nativeRows.find(n => n.entryId === r.entryId);
|
|
72
|
+
const entry = entries.find(e => e.id === r.entryId && e.options.id === d.ns && e.options.name === r.moduleName);
|
|
73
|
+
const schema = entry?.fiber?.runtime?.Config;
|
|
74
|
+
if (!native || !native.enabled || native.moduleName !== r.moduleName || native.readOnlyReason || !schema || !plain(d.value)) { warnings.push(`${d.ns}: native configuration is not safely addressable.`); continue; }
|
|
75
|
+
forms.push({ package: b.name, row: d.ns, module: r.moduleName, builtin: !b.installed,
|
|
76
|
+
value: d.value, secrets: d.secrets, revision: d.revision, schema, policy: policies.get(b.name)[d.ns] });
|
|
77
|
+
}
|
|
78
|
+
for (const p of packages) {
|
|
79
|
+
if (!p.readonly && !forms.some(f => f.package === p.name)) warnings.push(`${p.name}: no supported native editable Settings form; custom pages and storage are outside blueprint scope.`);
|
|
80
|
+
}
|
|
81
|
+
if (readFileSync(manifestPath, 'utf8') !== before) fail('stale', 'Profile changed while reading the catalog.');
|
|
82
|
+
const stamp = createHash('sha256').update(JSON.stringify({ manifest: before, packages, rows,
|
|
83
|
+
forms: forms.map(f => [identity(f), f.revision, f.policy ?? null]) })).digest('hex');
|
|
84
|
+
return { packages, order, rows, forms, warnings, stamp };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
snapshot,
|
|
88
|
+
inspect: spec => manager.inspect(spec),
|
|
89
|
+
allows: (form, at, proposed) => schemaAllows(form.schema, at, proposed),
|
|
90
|
+
async execute(op, signal) {
|
|
91
|
+
if (signal.aborted) fail('interrupted', 'Operation interrupted.');
|
|
92
|
+
if (op.type === 'install') {
|
|
93
|
+
const requestId = randomUUID();
|
|
94
|
+
// Script grants are never supplied by a blueprint. Native policy remains the authority.
|
|
95
|
+
const stop = () => { if (typeof manager.cancelInstall === 'function') void manager.cancelInstall(requestId).catch(() => {}); };
|
|
96
|
+
signal.addEventListener('abort', stop, { once: true });
|
|
97
|
+
try { return cleanResult(await manager.installBundle(`${op.name}@${op.version}`, { enabled: false, requestId })); }
|
|
98
|
+
finally { signal.removeEventListener('abort', stop); }
|
|
99
|
+
}
|
|
100
|
+
if (op.type === 'bundle') return cleanResult(await manager.setBundleEnabled(op.name, op.enabled));
|
|
101
|
+
if (op.type === 'row') return cleanResult(await manager.setPluginEnabled(op.entryId, op.enabled));
|
|
102
|
+
if (op.type === 'settings') { await ctx.settings.mutate(op.row, op.edits, op.revision); return { application: 'applied' }; }
|
|
103
|
+
fail('operation', 'Unsupported operation.');
|
|
104
|
+
},
|
|
105
|
+
verify(op, s) {
|
|
106
|
+
if (op.type === 'install') return s.packages.some(p => p.name === op.name && p.version === op.version && p.source === 'npm' && !p.reason);
|
|
107
|
+
if (op.type === 'bundle') return s.packages.some(p => p.name === op.name && p.enabled === op.enabled);
|
|
108
|
+
if (op.type === 'row') return s.rows.some(r => r.entryId === op.entryId && r.enabled === op.enabled);
|
|
109
|
+
const form = s.forms.find(f => identity(f) === identity(op));
|
|
110
|
+
return Boolean(form && op.edits.every(edit => equal(get(form.value, edit.path), edit.value)));
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@klarkxy/dsh-blueprint",
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
|
+
"description": "Import and export plugin blueprints from the official DSH plugin manager.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.mjs",
|
|
9
|
+
"./core": "./blueprint.mjs",
|
|
10
|
+
"./codec": "./codec.mjs",
|
|
11
|
+
"./client": "./client.js",
|
|
12
|
+
"./package.json": "./package.json",
|
|
13
|
+
"./locale/*.json": "./locale/*.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"*.mjs",
|
|
17
|
+
"client.js",
|
|
18
|
+
"cordis.patch.yml",
|
|
19
|
+
"locale/*.json",
|
|
20
|
+
"README*.md",
|
|
21
|
+
"NOTICE.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"ACCEPTANCE.md",
|
|
24
|
+
"SETTINGS-SCOPE.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node --check index.mjs && node --check client.js && node --check official.mjs && node --check engine.mjs && node --check blueprint.mjs && node --check codec.mjs",
|
|
28
|
+
"test": "node --test tests/*.test.mjs"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22",
|
|
32
|
+
"dsh": ">=0.1.7-0 <0.2.0"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"author": "klarkxy",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/klarkxy/dsh-plugins.git",
|
|
39
|
+
"directory": "plugins/dsh-blueprint"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://klarkxy.github.io/dsh-plugins/plugins/blueprint/",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"registry": "https://registry.npmjs.org/"
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"immediately": true,
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
55
|
+
"@deepseek-ai/dsh-client-locale",
|
|
56
|
+
"@deepseek-ai/dsh-client-connection"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"dshRelease": {
|
|
61
|
+
"contentHash": "f2612ba3162fb21206dfa2f290d95585fa4b289651784093535a47e1f9c91179"
|
|
62
|
+
}
|
|
63
|
+
}
|