@dsh-gis/dsh-opengis-skill 0.1.1 → 0.1.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/.editorconfig +19 -0
- package/CHANGELOG.md +96 -0
- package/README.md +89 -28
- package/THIRD_PARTY_NOTICES.md +8 -4
- package/cordis.patch.yml +6 -3
- package/lib/catalog.d.ts +50 -0
- package/lib/catalog.js +238 -0
- package/lib/index.d.ts +47 -6
- package/lib/index.js +32 -44
- package/lib/markdown-links.d.ts +11 -0
- package/lib/markdown-links.js +103 -0
- package/lib/skill-file.js +104 -17
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +11 -5
- package/upstream.json +64 -2
package/.editorconfig
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Editor defaults for the repository. Mirrors the LF normalization in
|
|
2
|
+
# .gitattributes; editors that support EditorConfig apply these automatically.
|
|
3
|
+
root = true
|
|
4
|
+
|
|
5
|
+
[*]
|
|
6
|
+
charset = utf-8
|
|
7
|
+
end_of_line = lf
|
|
8
|
+
insert_final_newline = true
|
|
9
|
+
trim_trailing_whitespace = true
|
|
10
|
+
|
|
11
|
+
[*.{js,mjs}]
|
|
12
|
+
indent_style = tab
|
|
13
|
+
|
|
14
|
+
[*.{json,jsonc,yaml,yml,md}]
|
|
15
|
+
indent_style = space
|
|
16
|
+
indent_size = 2
|
|
17
|
+
|
|
18
|
+
[*.md]
|
|
19
|
+
trim_trailing_whitespace = false
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.2] - 2026-10-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Tests for the frontmatter parser, the catalog builder, Markdown link resolution,
|
|
13
|
+
and the sync/validate scripts, plus expanded provider coverage in `test/plugin.test.mjs`.
|
|
14
|
+
- A SHA-256 integrity manifest in `upstream.json` (`schemaVersion: 1`,
|
|
15
|
+
`hashAlgorithm: "sha256"`, `files` keyed by the POSIX path relative to `assets/`),
|
|
16
|
+
so validation no longer needs an upstream checkout.
|
|
17
|
+
- Generated README sections: the skill count and skill table are rendered from the
|
|
18
|
+
vendored assets and verified by `npm run validate`.
|
|
19
|
+
- `.editorconfig` and this changelog.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Compatibility: `engines.dsh` widened to `>=0.2.0-rc.2 <0.3.0-0`. The registry API
|
|
24
|
+
and candidate/definition contract of `@deepseek-ai/dsh-skill` are byte-identical
|
|
25
|
+
between 0.2.0-rc.2 and 0.2.1-rc.2, and 0.2.0-rc.2 already supports
|
|
26
|
+
`dsh.bundle.patch`, locale and icon metadata, so the old `>=0.2.1-rc.2` gate
|
|
27
|
+
needlessly blocked users on the npm-latest DSH 0.2.0-rc.2.
|
|
28
|
+
- Runtime hardening: canonical provider registration via
|
|
29
|
+
`ctx.skills.registerProvider(() => provider)` (the returned disposer is owned by
|
|
30
|
+
the calling fiber, so no extra `ctx.effect` wrapper), a strict BOM-tolerant
|
|
31
|
+
frontmatter parser, a clear error when the asset root is missing, and
|
|
32
|
+
forward-slash `resourceBase` paths on Windows.
|
|
33
|
+
- Tooling hardening: two-way `sync:check` (drift plus upstream additions/removals),
|
|
34
|
+
SHA-256 manifest verification, generated README count/table verification,
|
|
35
|
+
validation of every relative Markdown link in the repository docs and assets,
|
|
36
|
+
and CRLF detection.
|
|
37
|
+
- CI: the test matrix now covers Ubuntu (Node 22/24) and Windows (Node 24), and a
|
|
38
|
+
new `sync` job verifies the vendored snapshot against the pinned upstream commit.
|
|
39
|
+
- `npm run check` (`validate && test`) added; `prepublishOnly` now runs it.
|
|
40
|
+
- Docs refreshed: README rewritten without hardcoded skill counts, with an English
|
|
41
|
+
summary, a compatibility note, install/uninstall/update notes, upstream integrity
|
|
42
|
+
docs and a troubleshooting section; package description and locale metadata
|
|
43
|
+
updated.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `SKILL.md` files with a UTF-8 BOM are parsed correctly; frontmatter parsing is
|
|
48
|
+
strict and reports unterminated quotes or block scalars instead of silently
|
|
49
|
+
ignoring them.
|
|
50
|
+
- A missing `assets/` directory now fails with a clear error that names the expected
|
|
51
|
+
asset root instead of an opaque `ENOENT`.
|
|
52
|
+
- `resourceBase` paths are emitted with forward slashes on Windows, so relative
|
|
53
|
+
references resolve consistently across platforms.
|
|
54
|
+
- Stale hardcoded skill counts removed from the README, locale metadata and the npm
|
|
55
|
+
description; count and table are generated from the vendored assets.
|
|
56
|
+
- CRLF line endings in vendored assets are detected and reported by validation.
|
|
57
|
+
- Sync refuses a `--from` directory that is not itself a git checkout root, so an
|
|
58
|
+
enclosing repository's HEAD can no longer be recorded as forged upstream
|
|
59
|
+
provenance; the manifest commit is now mandatory and validated.
|
|
60
|
+
- Sync rejects `gis/` directories without `SKILL.md` and stray files at the
|
|
61
|
+
`assets/` root instead of silently dropping or blessing them.
|
|
62
|
+
- Stray files at the `assets/` root are pruned by sync and rejected by
|
|
63
|
+
validation, so `assets/` publishes only skill directories.
|
|
64
|
+
- CI actions are pinned to release commit SHAs, and the external upstream
|
|
65
|
+
checkout disables persisted credentials.
|
|
66
|
+
- Validation checks the bundle row structurally, the locale file grammar, and the
|
|
67
|
+
engine range shape instead of accepting any non-empty string.
|
|
68
|
+
|
|
69
|
+
## [0.1.1] - 2026-10-19
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- Vendored skill assets are written with LF bytes regardless of the source
|
|
74
|
+
checkout's line endings, so the snapshot matches the upstream git blob on every
|
|
75
|
+
platform.
|
|
76
|
+
- Corrected the release documentation shipped with 0.1.0.
|
|
77
|
+
|
|
78
|
+
## [0.1.0] - 2026-10-19
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- Initial public release: a DeepSeek Harness bundle and plugin that vendors the
|
|
83
|
+
`gis/` skills of opengis-skills and registers one skill provider (`opengis`) on
|
|
84
|
+
`ctx.skills`.
|
|
85
|
+
- Bundled assets under `assets/<skill>/` (`SKILL.md` plus `reference/*.md`), bundle
|
|
86
|
+
wiring (`cordis.patch.yml`), locale metadata, icon, and npm packaging.
|
|
87
|
+
- Source-repository tooling: `npm run sync` / `npm run sync:check`, `npm run
|
|
88
|
+
validate`, `npm test`, and GitHub Actions CI.
|
|
89
|
+
|
|
90
|
+
## [0.0.0-stage] - 2026-10-19
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- First staging release: bundle/plugin skeleton, vendored skill assets, locale and
|
|
95
|
+
icon metadata, and the initial sync/validate/test scripts for installation
|
|
96
|
+
checks.
|
package/README.md
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
# @dsh-gis/dsh-opengis-skill
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **English summary** — A DeepSeek Harness bundle that packages the `gis/` skills of
|
|
4
|
+
> [opengis-skills](https://github.com/znlgis/opengis-skills) as installable, toggleable skills.
|
|
5
|
+
> The session catalog lists names and descriptions only; skill bodies and their
|
|
6
|
+
> `reference/*.md` files are loaded on demand. Zero runtime dependencies: every asset
|
|
7
|
+
> ships inside the npm package.
|
|
8
|
+
>
|
|
9
|
+
> Compatibility: DeepSeek Harness `>=0.2.0-rc.2 <0.3.0-0`; Node.js `>=22` for development
|
|
10
|
+
> only. MIT licensed.
|
|
11
|
+
|
|
12
|
+
DeepSeek Harness 的 **OpenGIS 技能包插件**:把 [opengis-skills](https://github.com/znlgis/opengis-skills) 中 `gis/` 分类的 GIS 技能打包成可安装、可启停的 DSH bundle。会话技能目录只列出名称与描述,技能正文与参考资料按需加载。
|
|
4
13
|
|
|
5
14
|
## 特性
|
|
6
15
|
|
|
7
|
-
-
|
|
16
|
+
- **覆盖 GIS 全链路**:GDAL、GDAL API、QGIS(qgis_process / PyQGIS / QGIS Server)、GeoServer(含 REST API 与 Cloud)、MapServer、PostGIS、PROJ、PDAL、GRASS GIS、JTS、NetTopologySuite、Esri Geometry API(Java/.NET)、Shapely、GeoPandas、GeoPipeAgent、OpenGisDAF、CesiumJS、OpenLayers、Leaflet、Mapbox GL JS、deck.gl、SharpMap、Mapsui、GeoTools、OpenGIS Utils(Java/.NET),以及 `opengis-all` 全流程索引。
|
|
8
17
|
- **按需加载**:注册的是 skill provider(`ctx.skills.registerProvider`),发现阶段只读 frontmatter;正文每次加载时读取,参考资料(`reference/*.md`)通过技能目录(resource base)相对解析。
|
|
9
|
-
- **零运行时依赖**:只用 Node
|
|
10
|
-
- **可同步上游**:`npm run sync` 从 opengis-skills 检出复制资产,记录上游 commit 到 `upstream.json
|
|
18
|
+
- **零运行时依赖**:只用 Node 内置模块,不 import DSH 内部包;全部资产随 npm 包分发,整个包解包后约 670 KiB。
|
|
19
|
+
- **可同步上游**:`npm run sync` 从 opengis-skills 检出复制资产,记录上游 commit 与每个文件的 SHA-256 到 `upstream.json`,并重新生成本文下方技能清单。
|
|
11
20
|
- **可启停**:单包同时是 bundle 与插件(`dsh.bundle.patch` 插入自身行),在 Plugins 里启用/停用即可。
|
|
12
21
|
|
|
22
|
+
## 兼容性
|
|
23
|
+
|
|
24
|
+
- **DeepSeek Harness**:`>=0.2.0-rc.2 <0.3.0-0`(`package.json` 的 `engines.dsh`)。本包通过 Host 提供的 `ctx.skills` 注册 provider,遵循 `@deepseek-ai/dsh-skill` 的 registry API 与 candidate/definition 契约;该契约在 0.2.0-rc.2 与 0.2.1-rc.2 之间字节一致,`dsh.bundle.patch`、locale 与图标元数据在 0.2.0-rc.2 已经可用。
|
|
25
|
+
- **Node.js**:`>=22`(`engines.node`)。仅开发脚本与测试需要,插件本身运行在 DSH 内。
|
|
26
|
+
- **运行时依赖**:无。`dependencies` 为空,只使用 Node 内置模块,不 import DSH 内部包。
|
|
27
|
+
|
|
13
28
|
## 技能清单
|
|
14
29
|
|
|
30
|
+
<!-- skills:count:start -->
|
|
31
|
+
共 **32** 个技能,描述取自上游 frontmatter。
|
|
32
|
+
<!-- skills:count:end -->
|
|
33
|
+
|
|
15
34
|
<!-- skills:start -->
|
|
16
35
|
| Skill | Description (upstream frontmatter) |
|
|
17
36
|
| --- | --- |
|
|
@@ -53,9 +72,10 @@ DeepSeek Harness 的 **OpenGIS 技能包插件**:把 [opengis-skills](https://
|
|
|
53
72
|
|
|
54
73
|
### 方式一:Plugins 界面(本地目录,适合开发)
|
|
55
74
|
|
|
56
|
-
1.
|
|
57
|
-
2.
|
|
58
|
-
3.
|
|
75
|
+
1. 在 DSH 中打开 **Plugins / 插件** 页面,选择安装 bundle(本地目录方式)。
|
|
76
|
+
2. 目标填**包根目录**的绝对路径,也就是包含 `package.json` 的目录,例如本仓库 `D:\self\code\dsh-opengis-skill`。
|
|
77
|
+
3. 安装后在列表中找到 **OpenGIS 技能包** 并启用;记录写入当前所选 profile 的插件配置。
|
|
78
|
+
4. 本地目录方式直接引用源码目录,改动会随重载生效,适合开发与试用;正式使用建议走下面的 npm 方式,便于升级与版本校验。
|
|
59
79
|
|
|
60
80
|
### 方式二:npm
|
|
61
81
|
|
|
@@ -63,11 +83,19 @@ DeepSeek Harness 的 **OpenGIS 技能包插件**:把 [opengis-skills](https://
|
|
|
63
83
|
dsh plugin --profile <profile> add @dsh-gis/dsh-opengis-skill
|
|
64
84
|
```
|
|
65
85
|
|
|
66
|
-
安装后在 Plugins 页面启用该 bundle
|
|
86
|
+
安装后在 Plugins 页面启用该 bundle。卸载:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
dsh plugin --profile <profile> remove @dsh-gis/dsh-opengis-skill
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- desktop profile 由 Electron 应用独占,推荐直接使用 Plugins 界面操作。
|
|
93
|
+
- 安装、升级或启停之后,需要重载/重启 dsh,并**新开会话**,技能目录才会反映最新状态(bundle 与技能目录在会话启动时装配)。
|
|
94
|
+
- 升级到新版本后同样需要重载 + 新会话;本地目录方式改动 `package.json` 或 `assets/` 后也一样。
|
|
67
95
|
|
|
68
96
|
## 使用
|
|
69
97
|
|
|
70
|
-
-
|
|
98
|
+
- 技能目录只列出每个技能的名称与描述;模型用 `skill("gdal")` 之类的调用加载,用户也可以在输入框直接输入 `/gdal`。
|
|
71
99
|
- 常用入口:
|
|
72
100
|
- `opengis-all` — 端到端 GIS 流程索引(GDAL → 处理 → GeoServer → 可视化);
|
|
73
101
|
- `gdal` / `gdal-api` — 命令行与编程式栅格/矢量处理;
|
|
@@ -75,10 +103,12 @@ dsh plugin --profile <profile> add @dsh-gis/dsh-opengis-skill
|
|
|
75
103
|
- `geoserver` + `geoserver-rest-api` — 发布与管理 OGC 服务;
|
|
76
104
|
- `postgis` — 空间数据库;`proj` — 坐标转换;
|
|
77
105
|
- `openlayers` / `cesiumjs` / `deck-gl` — Web 2D/3D 可视化。
|
|
78
|
-
- 加载结果会给出该技能的 Base directory
|
|
106
|
+
- 加载结果会给出该技能的 **Base directory**;正文中的 `reference/*.md` 与 `../<其他技能>/SKILL.md` 相对链接都据此解析。因为全部技能资产随同一个包一起分发,跨技能链接不需要额外安装或配置。
|
|
79
107
|
|
|
80
108
|
## 上游同步
|
|
81
109
|
|
|
110
|
+
> 本节只适用于**本仓库的源码开发**。`scripts/` 与 `test/` 不在 npm 发布包内(`package.json` 的 `files` 未包含它们),安装用户无需执行这些命令。
|
|
111
|
+
|
|
82
112
|
```bash
|
|
83
113
|
# 默认从 ../opengis-skills 读取,也可用 --from 或 OPENGIS_SKILLS_DIR 指定
|
|
84
114
|
npm run sync -- --from ../opengis-skills
|
|
@@ -87,45 +117,76 @@ npm run sync -- --from ../opengis-skills
|
|
|
87
117
|
npm run sync:check -- --from ../opengis-skills
|
|
88
118
|
```
|
|
89
119
|
|
|
90
|
-
同步脚本会:逐文件复制 `gis/<skill>/`(文本统一为 LF,保证与上游 git blob 跨平台字节一致)、校验 frontmatter(name 必须为 kebab-case 且与目录同名)、清理上游已删除的技能目录、更新 `upstream.json
|
|
120
|
+
同步脚本会:逐文件复制 `gis/<skill>/`(文本统一为 LF,保证与上游 git blob 跨平台字节一致)、校验 frontmatter(name 必须为 kebab-case 且与目录同名)、清理上游已删除的技能目录、更新 `upstream.json`,并重新生成 README 中的技能数量与技能表(生成区域由 `<!-- skills:count:* -->` 与 `<!-- skills:* -->` 标记界定)。`sync:check` 做双向比较:既检查已同步文件是否漂移,也检查上游新增/删除的技能是否遗漏。
|
|
121
|
+
|
|
122
|
+
为避免伪造来源,`npm run sync` 要求 `--from` 指向 opengis-skills 的 **git 检出根目录**:脚本用 `git rev-parse --show-toplevel` 校验该目录就是仓库根,避免把外层仓库的 HEAD 误记成上游 commit;无法证明 commit 时同步直接失败。上游 `gis/` 下若出现没有 `SKILL.md` 的目录,同步会报错而不是静默跳过(否则上游改名会静默丢技能);`gis/` 根目录的普通文件(上游分类索引 `gis/SKILL.md` 等)不参与同步,也不会被当作技能打包。
|
|
123
|
+
|
|
124
|
+
`upstream.json` 同时是一份完整性清单:`schemaVersion` 为 `1`,`hashAlgorithm` 为 `"sha256"`,`files` 以相对 `assets/` 的 POSIX 路径为键、对应文件的 SHA-256 摘要为值。`npm run validate` 会重新计算并比对全部摘要,因此**不依赖上游 checkout** 也能发现资产被手改、缺失或未列入清单的情况;CI 的 `sync` 任务再用固定 commit 的上游检出做一次 `sync:check`,确保快照与上游一致。
|
|
91
125
|
|
|
92
126
|
## 开发
|
|
93
127
|
|
|
128
|
+
需要 Node.js 22+。
|
|
129
|
+
|
|
94
130
|
```bash
|
|
95
|
-
npm
|
|
96
|
-
npm run validate # 资产、元数据、bundle
|
|
131
|
+
npm run check # validate + test,发布前必跑(也是 prepublishOnly)
|
|
132
|
+
npm run validate # 资产、元数据、bundle 接线、完整性清单与断链校验
|
|
133
|
+
npm test # Node 内置测试运行器
|
|
97
134
|
npm pack --dry-run # 查看实际发布内容
|
|
98
135
|
```
|
|
99
136
|
|
|
137
|
+
编辑器默认值见 [.editorconfig](./.editorconfig)(UTF-8、LF、最终换行;JS/MJS 用 Tab,JSON/YAML/Markdown 用 2 空格;Markdown 保留行尾空格)。版本变更记录见 [CHANGELOG.md](./CHANGELOG.md)。
|
|
138
|
+
|
|
100
139
|
### 目录结构
|
|
101
140
|
|
|
102
141
|
```text
|
|
103
142
|
dsh-opengis-skill/
|
|
104
|
-
├── package.json
|
|
105
|
-
├── cordis.patch.yml
|
|
143
|
+
├── package.json # dsh.bundle.patch、engines、files、图标/本地化导出
|
|
144
|
+
├── cordis.patch.yml # bundle 层:把 opengis-skill provider 行插入 Host
|
|
106
145
|
├── lib/
|
|
107
|
-
│ ├── index.js
|
|
108
|
-
│ ├──
|
|
109
|
-
│
|
|
110
|
-
├──
|
|
111
|
-
|
|
112
|
-
├──
|
|
113
|
-
├──
|
|
114
|
-
├──
|
|
115
|
-
|
|
146
|
+
│ ├── index.js # provider 插件(注册 ctx.skills、list/get)
|
|
147
|
+
│ ├── catalog.js # 扫描 assets/ 构建技能目录、生成 README 区域
|
|
148
|
+
│ ├── markdown-links.js # Markdown 相对链接解析与断链校验
|
|
149
|
+
│ ├── skill-file.js # SKILL.md frontmatter 解析(插件/脚本/测试共用)
|
|
150
|
+
│ └── *.d.ts # 手写类型声明
|
|
151
|
+
├── assets/<skill>/ # 技能资产(SKILL.md + reference/*.md,随上游同步)
|
|
152
|
+
├── locale/{en,zh}.json # Plugins 里显示的标题与描述
|
|
153
|
+
├── icon.svg # bundle 图标
|
|
154
|
+
├── scripts/
|
|
155
|
+
│ ├── sync-opengis-skills.mjs # 上游同步(assets/、upstream.json、README 生成区域)
|
|
156
|
+
│ └── validate-skills.mjs # 发布前校验(自包含,无需上游 checkout)
|
|
157
|
+
├── test/
|
|
158
|
+
│ ├── plugin.test.mjs # provider 目录/加载/链接
|
|
159
|
+
│ ├── skill-file.test.mjs # frontmatter 解析
|
|
160
|
+
│ ├── catalog.test.mjs # 目录构建与 README 生成区域
|
|
161
|
+
│ ├── markdown-links.test.mjs # 相对链接解析与断链检测
|
|
162
|
+
│ └── scripts.test.mjs # 同步/校验脚本(临时目录夹具)
|
|
163
|
+
├── .editorconfig # 编辑器默认值
|
|
164
|
+
├── CHANGELOG.md # 版本变更记录
|
|
165
|
+
├── README.md
|
|
166
|
+
└── upstream.json # 上游仓库、commit、同步时间、技能清单与 SHA-256 清单
|
|
116
167
|
```
|
|
117
168
|
|
|
118
169
|
### 设计说明
|
|
119
170
|
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
171
|
+
- **规范注册**:`apply` 时调用 `ctx.skills.registerProvider(() => provider)`;返回的 disposer 由调用方 fiber 拥有,卸载时自动清理,因此**不需要**再套一层 `ctx.effect(...)`。
|
|
172
|
+
- provider 名为 `opengis`,`source` 为 `opengis`,候选 rank 为 600(即 `BUNDLED_SKILL_RANK`,与官方内置技能包一致),`invocation` 为模型与用户均可调用。
|
|
173
|
+
- `list` 只返回元数据(名称、描述、rank、source、`locator`/`path`、`resourceBase`);`get` 每次重新读盘并解析正文,因此编辑资产后重新加载插件即可生效,无需清缓存。插件不注册工具、不注入提示段、不写会话状态。
|
|
174
|
+
- **运行时加固**:frontmatter 解析严格且容忍 UTF-8 BOM(`name` 必须为 kebab-case、`description` 必填,未闭合引号或块标量会报可读错误);`assets/` 缺失时报错会指明期望的资产根目录;`resourceBase` 在 Windows 上也统一输出为正斜杠,跨平台链接一致。
|
|
175
|
+
- 相对链接不做改写:包内是全量技能目录,`reference/*.md` 与 `../<skill>/SKILL.md` 都能解析;`npm run validate` 会扫描全部 Markdown 相对链接,断链或越出 `assets/` 都会失败。
|
|
176
|
+
|
|
177
|
+
## 故障排查
|
|
178
|
+
|
|
179
|
+
- **技能目录没有出现**:确认 bundle 已在 Plugins 中启用;安装、升级或启停后需要重载/重启 dsh 并新开会话。若用本地目录方式,确认填的是包含 `package.json` 的包根目录。
|
|
180
|
+
- **加载技能失败**:错误信息会带出具体资产路径(例如 `.../assets/gdal/SKILL.md` 的 frontmatter 问题);按提示修复,或从上游重新同步,不要手工编辑 `assets/`。
|
|
181
|
+
- **本地验证**:`npm run validate` 检查资产、元数据、bundle 接线、完整性清单与链接;`npm test` 跑单元测试;发布前统一用 `npm run check`。
|
|
182
|
+
- **同名技能冲突**:技能注册表按 rank、再按注册顺序决定同名技能的解析优先级;本包 rank 为 600。若其他 provider 也提供了同名技能,检查是否有另一份 OpenGIS 技能包或自定义技能被同时启用。
|
|
183
|
+
- **不要手改 `assets/`**:它是上游快照,手工修改会导致 SHA-256 完整性校验与同步检查失败;需要改内容时先改上游,再 `npm run sync` 重新生成。
|
|
123
184
|
|
|
124
185
|
## 与 @dsh-gis 其他插件的关系
|
|
125
186
|
|
|
126
187
|
- `@dsh-gis/dsh-gis`:GIS 工具与地图界面(inspect/query/CRS/OGC/UI)。
|
|
127
188
|
- `@dsh-gis/dsh-prompt-gis`:一段 GIS system-prompt 与两个内联技能(`gis-inspect-first`、`gis-query-patterns`)。
|
|
128
|
-
-
|
|
189
|
+
- 本包:纯知识型技能包,不提供工具,可与上述插件同时安装。
|
|
129
190
|
|
|
130
191
|
## 许可
|
|
131
192
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -31,10 +31,14 @@ SOFTWARE.
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
The exact upstream commit, sync time, and the list of vendored skills are
|
|
34
|
-
recorded in [`upstream.json`](./upstream.json)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
recorded in [`upstream.json`](./upstream.json), together with a SHA-256
|
|
35
|
+
integrity manifest of the snapshot: `schemaVersion` 1, `hashAlgorithm`
|
|
36
|
+
`sha256`, and a `files` map from every vendored file's POSIX path (relative
|
|
37
|
+
to `assets/`) to its digest. `npm run validate` recomputes those digests and
|
|
38
|
+
fails when the vendored bytes drift from the recorded manifest. The copy is
|
|
39
|
+
performed by `scripts/sync-opengis-skills.mjs`, which lives in the source
|
|
40
|
+
repository (<https://github.com/znlgis/dsh-opengis-skill>) and is not part of
|
|
41
|
+
the published package. Text files are vendored with LF line endings so the
|
|
38
42
|
snapshot matches the upstream git blob on every platform. The skills are
|
|
39
43
|
documentation: each one describes an independent open-source project (GDAL,
|
|
40
44
|
QGIS, GeoServer, PostGIS, JTS, CesiumJS, OpenLayers, and others) and links to
|
package/cordis.patch.yml
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# Bundle layer for @dsh-gis/dsh-opengis-skill.
|
|
2
2
|
#
|
|
3
3
|
# The row mounts the OpenGIS skill provider on the Host, so every preset that
|
|
4
|
-
# loads skills (the session catalog) sees the
|
|
5
|
-
# row
|
|
6
|
-
#
|
|
4
|
+
# loads skills (the session catalog) sees the bundled GIS skills. Disabling
|
|
5
|
+
# the row in the bundle configuration removes the provider, and disabling the
|
|
6
|
+
# bundle in Plugins removes the row; either way the session skill catalog
|
|
7
|
+
# follows the enabled bundle. The package and its assets are file-static, so
|
|
8
|
+
# a package update needs a reload or restart before the new version is
|
|
9
|
+
# visible.
|
|
7
10
|
- insert:
|
|
8
11
|
- id: opengis-skill
|
|
9
12
|
name: '@dsh-gis/dsh-opengis-skill'
|
package/lib/catalog.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** One validated skill entry read from disk. */
|
|
2
|
+
export interface SkillEntry {
|
|
3
|
+
/** Validated kebab-case skill name. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Required routing description. */
|
|
6
|
+
description: string;
|
|
7
|
+
/** Absolute skill directory. */
|
|
8
|
+
directory: string;
|
|
9
|
+
/** Absolute path to SKILL.md. */
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The instruction file name inside one skill directory. */
|
|
14
|
+
export declare const SKILL_FILE: "SKILL.md";
|
|
15
|
+
|
|
16
|
+
/** Read one skill directory, or undefined when it has no SKILL.md. */
|
|
17
|
+
export declare function readSkillEntry(directory: string, expectedName?: string): SkillEntry | undefined;
|
|
18
|
+
|
|
19
|
+
/** Read every skill directory under one asset root, sorted by name. */
|
|
20
|
+
export declare function readSkillCatalog(assetRoot: string): SkillEntry[];
|
|
21
|
+
|
|
22
|
+
/** Render one skill directory as a registry resource base. */
|
|
23
|
+
export declare function resourceBaseOf(directory: string): { kind: "directory"; path: string };
|
|
24
|
+
|
|
25
|
+
/** Render the generated README skill-count section. */
|
|
26
|
+
export declare function renderSkillCount(skills: readonly unknown[]): string;
|
|
27
|
+
|
|
28
|
+
/** Render the generated README skill table. */
|
|
29
|
+
export declare function renderSkillTable(skills: readonly { name: string; description: string }[]): string;
|
|
30
|
+
|
|
31
|
+
/** Read the generated README sections including their markers. */
|
|
32
|
+
export declare function readGeneratedSections(markdown: string): { count: string; table: string };
|
|
33
|
+
|
|
34
|
+
/** Regenerate both README sections in place. */
|
|
35
|
+
export declare function replaceGeneratedSections(
|
|
36
|
+
markdown: string,
|
|
37
|
+
skills: readonly { name: string; description: string }[]
|
|
38
|
+
): string;
|
|
39
|
+
|
|
40
|
+
/** List every regular file under one directory as sorted POSIX-style relative paths. */
|
|
41
|
+
export declare function listFiles(root: string): string[];
|
|
42
|
+
|
|
43
|
+
/** Hash one file with SHA-256 and return the lowercase hexadecimal digest. */
|
|
44
|
+
export declare function hashFile(path: string): string;
|
|
45
|
+
|
|
46
|
+
/** Hash every regular file under one directory. */
|
|
47
|
+
export declare function hashFiles(root: string): Record<string, string>;
|
|
48
|
+
|
|
49
|
+
/** Normalize one vendored file to the upstream git blob form (CRLF to LF, binary untouched). */
|
|
50
|
+
export declare function canonicalVendoredBytes(buffer: Buffer): Buffer;
|
package/lib/catalog.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared skill-catalog helpers for the runtime provider and the source-repo
|
|
3
|
+
* scripts. The functions are pure Node built-ins: no DeepSeek Harness imports
|
|
4
|
+
* and no third-party dependencies.
|
|
5
|
+
*
|
|
6
|
+
* @module @dsh-gis/dsh-opengis-skill/catalog
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
import { parseSkillFile } from "./skill-file.js";
|
|
13
|
+
|
|
14
|
+
/** The instruction file name inside one skill directory. */
|
|
15
|
+
export const SKILL_FILE = "SKILL.md";
|
|
16
|
+
|
|
17
|
+
/** One inline code-span delimiter used by the generated markdown table. */
|
|
18
|
+
const TICK = "`";
|
|
19
|
+
|
|
20
|
+
/** README marker pair around the generated skill count line. */
|
|
21
|
+
const COUNT_START = "<!-- skills:count:start -->";
|
|
22
|
+
const COUNT_END = "<!-- skills:count:end -->";
|
|
23
|
+
|
|
24
|
+
/** README marker pair around the generated skill table. */
|
|
25
|
+
const TABLE_START = "<!-- skills:start -->";
|
|
26
|
+
const TABLE_END = "<!-- skills:end -->";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read one skill directory.
|
|
30
|
+
*
|
|
31
|
+
* @param directory - absolute skill directory.
|
|
32
|
+
* @param expectedName - directory name the frontmatter name must match, when known.
|
|
33
|
+
* @returns the validated entry, or undefined when the directory has no SKILL.md.
|
|
34
|
+
* @throws when SKILL.md is malformed or declares a different name.
|
|
35
|
+
*/
|
|
36
|
+
export function readSkillEntry(directory, expectedName) {
|
|
37
|
+
const path = join(directory, SKILL_FILE);
|
|
38
|
+
if (statSync(path, { throwIfNoEntry: false })?.isFile() !== true) return undefined;
|
|
39
|
+
const parsed = parseSkillFile(readFileSync(path, "utf8"), path);
|
|
40
|
+
if (expectedName !== undefined && parsed.name !== expectedName) {
|
|
41
|
+
throw new Error(path + ' declares "' + parsed.name + '" but its directory is "' + expectedName + '"');
|
|
42
|
+
}
|
|
43
|
+
return { name: parsed.name, description: parsed.description, directory, path };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read every skill directory under one asset root, sorted by name.
|
|
48
|
+
*
|
|
49
|
+
* Directories without SKILL.md are skipped so a stray directory cannot break
|
|
50
|
+
* the runtime; the validation script reports those separately.
|
|
51
|
+
*
|
|
52
|
+
* @param assetRoot - absolute directory holding one subdirectory per skill.
|
|
53
|
+
* @returns the validated skills sorted by name.
|
|
54
|
+
* @throws when the root is missing, two skills share a name, or no skill exists.
|
|
55
|
+
*/
|
|
56
|
+
export function readSkillCatalog(assetRoot) {
|
|
57
|
+
if (statSync(assetRoot, { throwIfNoEntry: false })?.isDirectory() !== true) {
|
|
58
|
+
throw new Error("skill asset directory not found: " + assetRoot);
|
|
59
|
+
}
|
|
60
|
+
const skills = [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (const entry of readdirSync(assetRoot, { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isDirectory()) continue;
|
|
64
|
+
const skill = readSkillEntry(join(assetRoot, entry.name), entry.name);
|
|
65
|
+
if (skill === undefined) continue;
|
|
66
|
+
if (seen.has(skill.name)) throw new Error('duplicate skill name "' + skill.name + '" under ' + assetRoot);
|
|
67
|
+
seen.add(skill.name);
|
|
68
|
+
skills.push(skill);
|
|
69
|
+
}
|
|
70
|
+
if (skills.length === 0) throw new Error("no skills found under " + assetRoot);
|
|
71
|
+
return skills.sort((left, right) => compareCodePoints(left.name, right.name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Render the directory resource base with forward slashes for the model.
|
|
76
|
+
*
|
|
77
|
+
* @param directory - absolute skill directory, using the host separator.
|
|
78
|
+
* @returns the resource base value the skill registry expects.
|
|
79
|
+
*/
|
|
80
|
+
export function resourceBaseOf(directory) {
|
|
81
|
+
return { kind: "directory", path: directory.replaceAll("\\", "/") };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Render the generated README skill-count section.
|
|
86
|
+
*
|
|
87
|
+
* @param skills - the catalog entries.
|
|
88
|
+
* @returns the complete marked section, markers included.
|
|
89
|
+
*/
|
|
90
|
+
export function renderSkillCount(skills) {
|
|
91
|
+
return [
|
|
92
|
+
COUNT_START,
|
|
93
|
+
"共 **" + skills.length + "** 个技能,描述取自上游 frontmatter。",
|
|
94
|
+
COUNT_END
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Render the generated README skill table.
|
|
100
|
+
*
|
|
101
|
+
* @param skills - the catalog entries carrying name and description.
|
|
102
|
+
* @returns the complete marked table section, markers included.
|
|
103
|
+
*/
|
|
104
|
+
export function renderSkillTable(skills) {
|
|
105
|
+
const rows = skills.map((skill) => {
|
|
106
|
+
const cell = "| " + TICK + skill.name + TICK + " | " + skill.description.replaceAll("|", "\\|") + " |";
|
|
107
|
+
return cell;
|
|
108
|
+
});
|
|
109
|
+
return [
|
|
110
|
+
TABLE_START,
|
|
111
|
+
"| Skill | Description (upstream frontmatter) |",
|
|
112
|
+
"| --- | --- |",
|
|
113
|
+
...rows,
|
|
114
|
+
TABLE_END
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read the generated README sections including their markers.
|
|
120
|
+
*
|
|
121
|
+
* @param markdown - the complete README text.
|
|
122
|
+
* @returns the exact current count and table sections.
|
|
123
|
+
* @throws when a marker is missing or duplicated.
|
|
124
|
+
*/
|
|
125
|
+
export function readGeneratedSections(markdown) {
|
|
126
|
+
return {
|
|
127
|
+
count: extractSection(markdown, COUNT_START, COUNT_END),
|
|
128
|
+
table: extractSection(markdown, TABLE_START, TABLE_END)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Regenerate both README sections in place.
|
|
134
|
+
*
|
|
135
|
+
* @param markdown - the complete README text.
|
|
136
|
+
* @param skills - the catalog entries to render.
|
|
137
|
+
* @returns the README text with both generated sections replaced.
|
|
138
|
+
* @throws when a marker is missing or duplicated.
|
|
139
|
+
*/
|
|
140
|
+
export function replaceGeneratedSections(markdown, skills) {
|
|
141
|
+
const current = readGeneratedSections(markdown);
|
|
142
|
+
const replacements = [
|
|
143
|
+
[current.table, renderSkillTable(skills)],
|
|
144
|
+
[current.count, renderSkillCount(skills)]
|
|
145
|
+
];
|
|
146
|
+
let next = markdown;
|
|
147
|
+
for (const [from, to] of replacements) {
|
|
148
|
+
const index = next.indexOf(from);
|
|
149
|
+
if (index === -1) throw new Error("cannot regenerate README: generated section not found");
|
|
150
|
+
next = next.slice(0, index) + to + next.slice(index + from.length);
|
|
151
|
+
}
|
|
152
|
+
return next;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* List every regular file under one directory as POSIX-style relative paths.
|
|
157
|
+
*
|
|
158
|
+
* @param root - the directory to walk.
|
|
159
|
+
* @returns sorted relative paths using "/" on every platform.
|
|
160
|
+
* @throws when the root is missing, a symbolic link is present, or an entry is neither a file nor a directory.
|
|
161
|
+
*/
|
|
162
|
+
export function listFiles(root) {
|
|
163
|
+
const resolvedRoot = resolve(root);
|
|
164
|
+
const output = [];
|
|
165
|
+
const stack = [""];
|
|
166
|
+
while (stack.length > 0) {
|
|
167
|
+
const prefix = stack.pop();
|
|
168
|
+
const directory = prefix === "" ? resolvedRoot : join(resolvedRoot, prefix);
|
|
169
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
170
|
+
const relativePath = prefix === "" ? entry.name : prefix + "/" + entry.name;
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
stack.push(relativePath);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (entry.isSymbolicLink()) throw new Error("refusing to follow symbolic link: " + join(directory, entry.name));
|
|
176
|
+
if (!entry.isFile()) throw new Error("unsupported file type: " + join(directory, entry.name));
|
|
177
|
+
output.push(relativePath);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return output.sort(compareCodePoints);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Hash one file with SHA-256.
|
|
185
|
+
*
|
|
186
|
+
* @param path - the file to read.
|
|
187
|
+
* @returns the lowercase hexadecimal digest.
|
|
188
|
+
*/
|
|
189
|
+
export function hashFile(path) {
|
|
190
|
+
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Hash every regular file under one directory.
|
|
195
|
+
*
|
|
196
|
+
* @param root - the directory to walk.
|
|
197
|
+
* @returns a map from POSIX-style relative path to lowercase SHA-256 digest.
|
|
198
|
+
*/
|
|
199
|
+
export function hashFiles(root) {
|
|
200
|
+
const digests = {};
|
|
201
|
+
for (const relativePath of listFiles(root)) digests[relativePath] = hashFile(join(root, relativePath));
|
|
202
|
+
return digests;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalize one vendored file to the upstream git blob form.
|
|
207
|
+
*
|
|
208
|
+
* A file carrying a NUL byte is treated as binary and copied unchanged; every
|
|
209
|
+
* other file has its CRLF line endings collapsed to LF. An upstream checkout on
|
|
210
|
+
* Windows can carry CRLF through core.autocrlf while the upstream git blob is
|
|
211
|
+
* LF, so normalizing keeps the vendored bytes equal to the blob independent of
|
|
212
|
+
* the platform.
|
|
213
|
+
*
|
|
214
|
+
* @param buffer - the raw file bytes.
|
|
215
|
+
* @returns the bytes to write or compare.
|
|
216
|
+
*/
|
|
217
|
+
export function canonicalVendoredBytes(buffer) {
|
|
218
|
+
if (buffer.includes(0)) return buffer;
|
|
219
|
+
return Buffer.from(buffer.toString("utf8").replaceAll("\r\n", "\n"), "utf8");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Extract one marked section and reject missing or duplicated markers. */
|
|
223
|
+
function extractSection(markdown, start, end) {
|
|
224
|
+
const first = markdown.indexOf(start);
|
|
225
|
+
if (first === -1) throw new Error('missing generated README marker "' + start + '"');
|
|
226
|
+
if (markdown.indexOf(start, first + start.length) !== -1) throw new Error('duplicate generated README marker "' + start + '"');
|
|
227
|
+
const last = markdown.indexOf(end, first + start.length);
|
|
228
|
+
if (last === -1) throw new Error('missing generated README marker "' + end + '"');
|
|
229
|
+
if (markdown.indexOf(end, last + end.length) !== -1) throw new Error('duplicate generated README marker "' + end + '"');
|
|
230
|
+
return markdown.slice(first, last + end.length);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Compare two strings by Unicode code point, independent of the locale. */
|
|
234
|
+
function compareCodePoints(left, right) {
|
|
235
|
+
if (left < right) return -1;
|
|
236
|
+
if (left > right) return 1;
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
/** Stable Loader identity. */
|
|
2
2
|
export declare const name: "opengis-skill";
|
|
3
3
|
|
|
4
|
-
/** Services that must exist before
|
|
4
|
+
/** Services that must exist before "apply" runs. */
|
|
5
5
|
export declare const inject: readonly ["skills"];
|
|
6
6
|
|
|
7
|
+
/** Invocation controls shared by the model-facing and user-facing surfaces. */
|
|
8
|
+
export interface OpenGisInvocation {
|
|
9
|
+
modelInvocable: boolean;
|
|
10
|
+
userInvocable: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Directory resource base handed to the model with a loaded skill. */
|
|
14
|
+
export interface OpenGisResourceBase {
|
|
15
|
+
kind: "directory";
|
|
16
|
+
path: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One skill summary returned by the provider catalog. */
|
|
20
|
+
export interface OpenGisSkillCandidate {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
invocation: OpenGisInvocation;
|
|
24
|
+
source: string;
|
|
25
|
+
provider: string;
|
|
26
|
+
rank: number;
|
|
27
|
+
locator: string;
|
|
28
|
+
path: string;
|
|
29
|
+
resourceBase: OpenGisResourceBase;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** One loaded skill body returned by the provider. */
|
|
33
|
+
export interface OpenGisSkillDefinition {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
invocation: OpenGisInvocation;
|
|
37
|
+
source: string;
|
|
38
|
+
provider: string;
|
|
39
|
+
resourceBase: OpenGisResourceBase;
|
|
40
|
+
path: string;
|
|
41
|
+
content: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The borrowed provider object the skill registry holds. */
|
|
45
|
+
export interface OpenGisSkillProvider {
|
|
46
|
+
name: string;
|
|
47
|
+
list(options?: { signal?: AbortSignal }): Promise<readonly OpenGisSkillCandidate[]>;
|
|
48
|
+
get(candidate: OpenGisSkillCandidate, options?: { signal?: AbortSignal }): Promise<OpenGisSkillDefinition>;
|
|
49
|
+
}
|
|
50
|
+
|
|
7
51
|
/** Cordis context surface used by this plugin. */
|
|
8
52
|
export interface OpenGisSkillHost {
|
|
9
|
-
/** Own a registration so plugin unload removes it. */
|
|
10
|
-
effect(callback: () => unknown, label?: string): unknown;
|
|
11
|
-
/** The skill registry this provider registers on. */
|
|
12
53
|
skills: {
|
|
13
|
-
registerProvider(create: (control: { signal: AbortSignal; invalidate(): void }) =>
|
|
54
|
+
registerProvider(create: (control: { signal: AbortSignal; invalidate(): void }) => OpenGisSkillProvider): unknown;
|
|
14
55
|
};
|
|
15
56
|
}
|
|
16
57
|
|
|
17
|
-
/** Register the bundled OpenGIS skill provider on
|
|
58
|
+
/** Register the bundled OpenGIS skill provider on "ctx.skills". */
|
|
18
59
|
export declare function apply(ctx: OpenGisSkillHost): void;
|
package/lib/index.js
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bundled OpenGIS skill provider for DeepSeek Harness.
|
|
3
3
|
*
|
|
4
|
-
* The package ships the
|
|
5
|
-
*
|
|
4
|
+
* The package ships the "gis/" skills of the opengis-skills repository under
|
|
5
|
+
* "assets/<skill>/SKILL.md" and registers one skill provider on "ctx.skills".
|
|
6
6
|
* Discovery reads only the frontmatter; the instruction body and the
|
|
7
|
-
*
|
|
7
|
+
* "reference/*.md" files are read on demand, and each candidate carries the
|
|
8
8
|
* skill directory as its resource base so relative references resolve.
|
|
9
9
|
*
|
|
10
10
|
* @module @dsh-gis/dsh-opengis-skill
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
14
13
|
import { readFile } from "node:fs/promises";
|
|
15
|
-
import { join, sep } from "node:path";
|
|
16
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { readSkillCatalog, resourceBaseOf } from "./catalog.js";
|
|
17
16
|
import { parseSkillFile } from "./skill-file.js";
|
|
18
17
|
|
|
19
|
-
/** Provider name registered on
|
|
18
|
+
/** Provider name registered on "ctx.skills" (the runtime name "runtime" is reserved). */
|
|
20
19
|
const PROVIDER_NAME = "opengis";
|
|
21
20
|
|
|
22
21
|
/** Source label carried by every candidate and definition. */
|
|
@@ -25,59 +24,43 @@ const SOURCE = "opengis";
|
|
|
25
24
|
/** Standard rank for packaged skill providers and bundled roots (BUNDLED_SKILL_RANK). */
|
|
26
25
|
const RANK = 600;
|
|
27
26
|
|
|
28
|
-
/** The instruction file name inside one skill directory. */
|
|
29
|
-
const SKILL_FILE = "SKILL.md";
|
|
30
|
-
|
|
31
27
|
/** Absolute asset root, resolved from this module inside the installed package. */
|
|
32
28
|
const ASSET_ROOT = fileURLToPath(new URL("../assets/", import.meta.url));
|
|
33
29
|
|
|
34
30
|
/** Stable Loader identity. */
|
|
35
31
|
const name = "opengis-skill";
|
|
36
32
|
|
|
37
|
-
/** The skill registry must exist before
|
|
33
|
+
/** The skill registry must exist before "apply" runs. */
|
|
38
34
|
const inject = ["skills"];
|
|
39
35
|
|
|
36
|
+
/** The invocation policy every bundled skill uses: model and user may load it. */
|
|
37
|
+
const INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
38
|
+
|
|
40
39
|
/**
|
|
41
|
-
* Read
|
|
40
|
+
* Read and validate the packaged skill catalog, annotating asset failures with
|
|
41
|
+
* the plugin name so a broken installation is diagnosable from the host log.
|
|
42
42
|
*
|
|
43
|
-
* @
|
|
44
|
-
* @
|
|
45
|
-
* @throws when two skills share a name or a directory disagrees with its frontmatter.
|
|
43
|
+
* @returns the validated catalog sorted by name.
|
|
44
|
+
* @throws when the packaged assets are missing or malformed.
|
|
46
45
|
*/
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const directory = join(assetRoot, entry.name);
|
|
53
|
-
const path = join(directory, SKILL_FILE);
|
|
54
|
-
if (statSync(path, { throwIfNoEntry: false })?.isFile() !== true) continue;
|
|
55
|
-
const parsed = parseSkillFile(readFileSync(path, "utf8"), path);
|
|
56
|
-
if (parsed.name !== entry.name) {
|
|
57
|
-
throw new Error(`opengis-skill: ${path} declares "${parsed.name}" but its directory is "${entry.name}"`);
|
|
58
|
-
}
|
|
59
|
-
if (seen.has(parsed.name)) throw new Error(`opengis-skill: duplicate skill name "${parsed.name}"`);
|
|
60
|
-
seen.add(parsed.name);
|
|
61
|
-
skills.push({ name: parsed.name, description: parsed.description, directory, path });
|
|
46
|
+
function loadSkills() {
|
|
47
|
+
try {
|
|
48
|
+
return readSkillCatalog(ASSET_ROOT);
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
throw new Error("opengis-skill: cannot load skill assets from " + ASSET_ROOT + ": " + cause.message, { cause });
|
|
62
51
|
}
|
|
63
|
-
return skills.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Render the directory resource base with forward slashes for the model. */
|
|
67
|
-
function resourceBaseOf(directory) {
|
|
68
|
-
return { kind: "directory", path: directory.split(sep).join("/") };
|
|
69
52
|
}
|
|
70
53
|
|
|
71
|
-
/** The invocation policy every bundled skill uses: model and user may load it. */
|
|
72
|
-
const INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
73
|
-
|
|
74
54
|
/**
|
|
75
|
-
* Register the bundled OpenGIS skill provider on
|
|
55
|
+
* Register the bundled OpenGIS skill provider on "ctx.skills".
|
|
56
|
+
*
|
|
57
|
+
* The returned disposer of "registerProvider" is owned by the calling fiber,
|
|
58
|
+
* so plugin unload removes the provider without an extra "ctx.effect" wrapper.
|
|
76
59
|
*
|
|
77
60
|
* @param ctx - Cordis context carrying the skill registry.
|
|
78
61
|
*/
|
|
79
62
|
function apply(ctx) {
|
|
80
|
-
const skills =
|
|
63
|
+
const skills = loadSkills();
|
|
81
64
|
const byName = new Map(skills.map((skill) => [skill.name, skill]));
|
|
82
65
|
const candidates = skills.map((skill) => ({
|
|
83
66
|
name: skill.name,
|
|
@@ -95,11 +78,16 @@ function apply(ctx) {
|
|
|
95
78
|
list: () => Promise.resolve(candidates),
|
|
96
79
|
async get(candidate, options) {
|
|
97
80
|
const skill = byName.get(candidate.name);
|
|
98
|
-
if (skill === undefined) throw new Error(
|
|
81
|
+
if (skill === undefined) throw new Error('opengis-skill: unknown skill "' + candidate.name + '"');
|
|
99
82
|
const raw = await readFile(skill.path, { encoding: "utf8", signal: options?.signal });
|
|
100
|
-
|
|
83
|
+
let parsed;
|
|
84
|
+
try {
|
|
85
|
+
parsed = parseSkillFile(raw, skill.path);
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
throw new Error("opengis-skill: " + cause.message, { cause });
|
|
88
|
+
}
|
|
101
89
|
if (parsed.name !== candidate.name) {
|
|
102
|
-
throw new Error(
|
|
90
|
+
throw new Error("opengis-skill: " + skill.path + ' now declares "' + parsed.name + '" but was listed as "' + candidate.name + '"');
|
|
103
91
|
}
|
|
104
92
|
return {
|
|
105
93
|
name: parsed.name,
|
|
@@ -113,7 +101,7 @@ function apply(ctx) {
|
|
|
113
101
|
};
|
|
114
102
|
}
|
|
115
103
|
};
|
|
116
|
-
ctx.
|
|
104
|
+
ctx.skills.registerProvider(() => provider);
|
|
117
105
|
}
|
|
118
106
|
|
|
119
107
|
export { apply, inject, name };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Extract every relative file link target from one markdown document. */
|
|
2
|
+
export declare function relativeMarkdownLinks(markdown: string): string[];
|
|
3
|
+
|
|
4
|
+
/** Resolve one relative markdown link against its containing file. */
|
|
5
|
+
export declare function resolveMarkdownLink(fromFile: string, link: string): { path: string; fragment: string };
|
|
6
|
+
|
|
7
|
+
/** Report whether one resolved path stays inside a directory. */
|
|
8
|
+
export declare function isInsideDirectory(root: string, candidate: string): boolean;
|
|
9
|
+
|
|
10
|
+
/** Collect every problem with the relative links in one markdown document. */
|
|
11
|
+
export declare function findMarkdownLinkProblems(fromFile: string, markdown: string, root: string): string[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown link helpers shared by the validation script and the tests.
|
|
3
|
+
*
|
|
4
|
+
* Only relative file links are inspected: scheme links, fragment-only links,
|
|
5
|
+
* and absolute URLs are ignored. Resolution is lexical, so a link must stay
|
|
6
|
+
* inside the directory the caller allows.
|
|
7
|
+
*
|
|
8
|
+
* @module @dsh-gis/dsh-opengis-skill/markdown-links
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { dirname, resolve, sep } from "node:path";
|
|
13
|
+
|
|
14
|
+
/** One inline markdown link target, as written. */
|
|
15
|
+
const LINK = /\]\(([^)]+)\)/gu;
|
|
16
|
+
|
|
17
|
+
/** A link with a URI scheme or a fragment-only target is not a file link. */
|
|
18
|
+
const NON_FILE_LINK = /^(?:[a-z][a-z0-9+.-]*:|#)/iu;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract every relative file link target from one markdown document.
|
|
22
|
+
*
|
|
23
|
+
* @param markdown - the complete markdown text.
|
|
24
|
+
* @returns the targets still carrying their optional fragment or query text.
|
|
25
|
+
*/
|
|
26
|
+
export function relativeMarkdownLinks(markdown) {
|
|
27
|
+
const links = [];
|
|
28
|
+
for (const match of markdown.matchAll(LINK)) {
|
|
29
|
+
const target = match[1].trim();
|
|
30
|
+
if (target.length === 0 || NON_FILE_LINK.test(target)) continue;
|
|
31
|
+
links.push(target);
|
|
32
|
+
}
|
|
33
|
+
return links;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve one relative markdown link against its containing file.
|
|
38
|
+
*
|
|
39
|
+
* @param fromFile - the markdown file that contains the link.
|
|
40
|
+
* @param link - a relative link target, optionally with a "#fragment".
|
|
41
|
+
* @returns the absolute target path and the fragment without its "#".
|
|
42
|
+
* @throws when the target contains invalid percent-encoding.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveMarkdownLink(fromFile, link) {
|
|
45
|
+
const hash = link.indexOf("#");
|
|
46
|
+
const pathPart = hash === -1 ? link : link.slice(0, hash);
|
|
47
|
+
const fragment = hash === -1 ? "" : link.slice(hash + 1);
|
|
48
|
+
if (pathPart.length === 0) return { path: fromFile, fragment };
|
|
49
|
+
let decoded;
|
|
50
|
+
try {
|
|
51
|
+
decoded = decodeURIComponent(pathPart);
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
throw new Error('invalid URL encoding in link "' + link + '"', { cause });
|
|
54
|
+
}
|
|
55
|
+
if (decoded.length === 0) return { path: fromFile, fragment };
|
|
56
|
+
return { path: resolve(dirname(fromFile), decoded), fragment };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Report whether one resolved path stays inside a directory.
|
|
61
|
+
*
|
|
62
|
+
* @param root - the allowed directory.
|
|
63
|
+
* @param candidate - the resolved path to test.
|
|
64
|
+
* @returns whether the candidate equals or is below the root.
|
|
65
|
+
*/
|
|
66
|
+
export function isInsideDirectory(root, candidate) {
|
|
67
|
+
const rootKey = pathKey(root);
|
|
68
|
+
const candidateKey = pathKey(candidate);
|
|
69
|
+
return candidateKey === rootKey || candidateKey.startsWith(rootKey + sep);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Collect every problem with the relative links in one markdown document.
|
|
74
|
+
*
|
|
75
|
+
* @param fromFile - the markdown file that contains the links; also used in messages.
|
|
76
|
+
* @param markdown - the complete markdown text.
|
|
77
|
+
* @param root - the directory every link must stay inside.
|
|
78
|
+
* @returns human-readable problems, empty when every link is valid.
|
|
79
|
+
*/
|
|
80
|
+
export function findMarkdownLinkProblems(fromFile, markdown, root) {
|
|
81
|
+
const problems = [];
|
|
82
|
+
for (const link of relativeMarkdownLinks(markdown)) {
|
|
83
|
+
let resolved;
|
|
84
|
+
try {
|
|
85
|
+
resolved = resolveMarkdownLink(fromFile, link);
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
problems.push(fromFile + ": " + cause.message);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!isInsideDirectory(root, resolved.path)) {
|
|
91
|
+
problems.push(fromFile + ": link escapes " + root + ": " + link);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!existsSync(resolved.path)) problems.push(fromFile + ": broken link " + link);
|
|
95
|
+
}
|
|
96
|
+
return problems;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Normalize one path for comparison, folding case on Windows. */
|
|
100
|
+
function pathKey(value) {
|
|
101
|
+
const normalized = resolve(value);
|
|
102
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
103
|
+
}
|
package/lib/skill-file.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal SKILL.md frontmatter reader shared by the plugin, the sync script,
|
|
3
|
-
* and the validation script.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* and the validation script.
|
|
4
|
+
*
|
|
5
|
+
* Vendored opengis-skills files keep "name" and "description" on single lines,
|
|
6
|
+
* so only scalar fields are read; list fields such as "tags" are ignored.
|
|
7
|
+
* Multi-line block scalars are rejected instead of being silently truncated,
|
|
8
|
+
* and double-quoted scalars support the common YAML escape sequences.
|
|
6
9
|
*
|
|
7
10
|
* @module @dsh-gis/dsh-opengis-skill/skill-file
|
|
8
11
|
*/
|
|
@@ -13,9 +16,39 @@ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u;
|
|
|
13
16
|
/** The public kebab-case skill-name grammar used by the skill registry. */
|
|
14
17
|
const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
15
18
|
|
|
16
|
-
/** One
|
|
19
|
+
/** One "key: value" scalar line inside the frontmatter block. */
|
|
17
20
|
const FIELD = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/u;
|
|
18
21
|
|
|
22
|
+
/** A complete single-line double-quoted YAML scalar. */
|
|
23
|
+
const DOUBLE_QUOTED = /^"((?:[^"\\]|\\.)*)"$/u;
|
|
24
|
+
|
|
25
|
+
/** A complete single-line single-quoted YAML scalar. */
|
|
26
|
+
const SINGLE_QUOTED = /^'((?:[^']|'')*)'$/u;
|
|
27
|
+
|
|
28
|
+
/** One double-quoted escape sequence: \uXXXX, \UXXXXXXXX, \xXX, or \c. */
|
|
29
|
+
const DOUBLE_QUOTED_ESCAPE = /\\(u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|x[0-9A-Fa-f]{2}|[\s\S])/gu;
|
|
30
|
+
|
|
31
|
+
/** The one-character YAML escape sequences this reader understands. */
|
|
32
|
+
const SIMPLE_ESCAPES = new Map([
|
|
33
|
+
["0", "\0"],
|
|
34
|
+
["a", "\x07"],
|
|
35
|
+
["b", "\b"],
|
|
36
|
+
["t", "\t"],
|
|
37
|
+
["n", "\n"],
|
|
38
|
+
["v", "\v"],
|
|
39
|
+
["f", "\f"],
|
|
40
|
+
["r", "\r"],
|
|
41
|
+
["e", "\x1b"],
|
|
42
|
+
[" ", " "],
|
|
43
|
+
['"', '"'],
|
|
44
|
+
["/", "/"],
|
|
45
|
+
["\\", "\\"],
|
|
46
|
+
["N", "\u0085"],
|
|
47
|
+
["_", "\u00a0"],
|
|
48
|
+
["L", "\u2028"],
|
|
49
|
+
["P", "\u2029"]
|
|
50
|
+
]);
|
|
51
|
+
|
|
19
52
|
/**
|
|
20
53
|
* Parse one SKILL.md file into the registry fields and the instruction body.
|
|
21
54
|
*
|
|
@@ -25,19 +58,31 @@ const FIELD = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/u;
|
|
|
25
58
|
* @throws when the frontmatter, the name, or the description is missing or malformed.
|
|
26
59
|
*/
|
|
27
60
|
export function parseSkillFile(raw, path) {
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
if (typeof raw !== "string") throw new TypeError(path + ": SKILL.md content must be a string");
|
|
62
|
+
const text = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
63
|
+
const match = FRONTMATTER.exec(text);
|
|
64
|
+
if (match === null) throw new Error(path + ": missing YAML frontmatter");
|
|
30
65
|
const fields = new Map();
|
|
31
66
|
for (const line of match[1].split(/\r?\n/u)) {
|
|
32
67
|
const entry = FIELD.exec(line);
|
|
33
68
|
if (entry === null) continue;
|
|
34
|
-
fields.set(entry[1],
|
|
69
|
+
fields.set(entry[1], parseScalar(entry[2].trim(), path + ': "' + entry[1] + '"'));
|
|
35
70
|
}
|
|
36
71
|
const name = fields.get("name");
|
|
37
72
|
const description = fields.get("description");
|
|
38
|
-
if (!isSkillName(name)) throw new Error(
|
|
39
|
-
if (typeof description !== "string" || description.length === 0) throw new Error(
|
|
40
|
-
return { name, description, content:
|
|
73
|
+
if (!isSkillName(name)) throw new Error(path + ': "name" must be kebab-case, got ' + JSON.stringify(name));
|
|
74
|
+
if (typeof description !== "string" || description.length === 0) throw new Error(path + ': "description" is required');
|
|
75
|
+
return { name, description, content: normalizeBody(text.slice(match[0].length)) };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalize the instruction body: trim the edges and use LF line endings.
|
|
80
|
+
*
|
|
81
|
+
* @param body - the raw text after the closing frontmatter marker.
|
|
82
|
+
* @returns the instruction body handed to the skill registry.
|
|
83
|
+
*/
|
|
84
|
+
function normalizeBody(body) {
|
|
85
|
+
return body.trim().replace(/\r\n?/gu, "\n");
|
|
41
86
|
}
|
|
42
87
|
|
|
43
88
|
/**
|
|
@@ -50,13 +95,55 @@ export function isSkillName(value) {
|
|
|
50
95
|
return typeof value === "string" && SKILL_NAME.test(value);
|
|
51
96
|
}
|
|
52
97
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Parse one frontmatter scalar value.
|
|
100
|
+
*
|
|
101
|
+
* Only single-line scalars are supported. A leading quote must close on the
|
|
102
|
+
* same line, and a block scalar indicator ("|" or ">") is rejected so that an
|
|
103
|
+
* upstream format change fails loudly instead of silently truncating a
|
|
104
|
+
* description.
|
|
105
|
+
*
|
|
106
|
+
* @param value - the trimmed text after "key:".
|
|
107
|
+
* @param subject - the error-message prefix, for example "file: \"description\"".
|
|
108
|
+
* @returns the decoded scalar text.
|
|
109
|
+
*/
|
|
110
|
+
function parseScalar(value, subject) {
|
|
111
|
+
if (value.length === 0) return "";
|
|
112
|
+
if (value.startsWith('"')) {
|
|
113
|
+
const quoted = DOUBLE_QUOTED.exec(value);
|
|
114
|
+
if (quoted === null) throw new Error(subject + ": malformed or unterminated double-quoted scalar");
|
|
115
|
+
return unescapeDoubleQuoted(quoted[1], subject);
|
|
116
|
+
}
|
|
117
|
+
if (value.startsWith("'")) {
|
|
118
|
+
const quoted = SINGLE_QUOTED.exec(value);
|
|
119
|
+
if (quoted === null) throw new Error(subject + ": malformed or unterminated single-quoted scalar");
|
|
120
|
+
return quoted[1].replaceAll("''", "'");
|
|
57
121
|
}
|
|
58
|
-
if (value.
|
|
59
|
-
|
|
122
|
+
if (value.startsWith(">") || value.startsWith("|")) {
|
|
123
|
+
throw new Error(subject + ": multi-line block scalars are not supported");
|
|
60
124
|
}
|
|
61
|
-
|
|
125
|
+
const comment = value.search(/[ \t]#/u);
|
|
126
|
+
return (comment === -1 ? value : value.slice(0, comment)).trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Decode the body of a double-quoted YAML scalar.
|
|
131
|
+
*
|
|
132
|
+
* @param body - the text between the surrounding quotes.
|
|
133
|
+
* @param subject - the error-message prefix.
|
|
134
|
+
* @returns the decoded text.
|
|
135
|
+
*/
|
|
136
|
+
function unescapeDoubleQuoted(body, subject) {
|
|
137
|
+
return body.replace(DOUBLE_QUOTED_ESCAPE, (match, escape) => {
|
|
138
|
+
if (escape.length > 1) {
|
|
139
|
+
const codePoint = Number.parseInt(escape.slice(1), 16);
|
|
140
|
+
if (!Number.isInteger(codePoint) || codePoint > 0x10ffff) {
|
|
141
|
+
throw new Error(subject + ': invalid unicode escape "\\' + escape + '"');
|
|
142
|
+
}
|
|
143
|
+
return String.fromCodePoint(codePoint);
|
|
144
|
+
}
|
|
145
|
+
const simple = SIMPLE_ESCAPES.get(escape);
|
|
146
|
+
if (simple === undefined) throw new Error(subject + ': unsupported escape sequence "\\' + escape + '"');
|
|
147
|
+
return simple;
|
|
148
|
+
});
|
|
62
149
|
}
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "OpenGIS Skills",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "GIS skills from opengis-skills — GDAL, QGIS, GeoServer, PostGIS, JTS, CesiumJS, OpenLayers, Leaflet, deck.gl and more — loaded on demand."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "OpenGIS 技能包",
|
|
4
|
-
"description": "来自 opengis-skills 的
|
|
4
|
+
"description": "来自 opengis-skills 的 GIS 技能:GDAL、QGIS、GeoServer、PostGIS、JTS、CesiumJS、OpenLayers、Leaflet、deck.gl 等,按需加载。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-gis/dsh-opengis-skill",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "OpenGIS skill pack for DeepSeek Harness:
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "OpenGIS skill pack for DeepSeek Harness: the gis/ skills from opengis-skills (GDAL, QGIS, GeoServer, PostGIS, JTS, CesiumJS, OpenLayers and more), loaded on demand.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
"icon.svg",
|
|
29
29
|
"upstream.json",
|
|
30
30
|
"README.md",
|
|
31
|
-
"THIRD_PARTY_NOTICES.md"
|
|
31
|
+
"THIRD_PARTY_NOTICES.md",
|
|
32
|
+
"CHANGELOG.md",
|
|
33
|
+
".editorconfig"
|
|
32
34
|
],
|
|
33
35
|
"engines": {
|
|
34
|
-
"dsh": ">=0.2.
|
|
36
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0-0",
|
|
35
37
|
"node": ">=22"
|
|
36
38
|
},
|
|
37
39
|
"dsh": {
|
|
@@ -44,7 +46,8 @@
|
|
|
44
46
|
"sync:check": "node scripts/sync-opengis-skills.mjs --check",
|
|
45
47
|
"validate": "node scripts/validate-skills.mjs",
|
|
46
48
|
"test": "node --test",
|
|
47
|
-
"
|
|
49
|
+
"check": "npm run validate && npm test",
|
|
50
|
+
"prepublishOnly": "npm run check"
|
|
48
51
|
},
|
|
49
52
|
"repository": {
|
|
50
53
|
"type": "git",
|
|
@@ -60,7 +63,10 @@
|
|
|
60
63
|
"dsh-plugin",
|
|
61
64
|
"cordis",
|
|
62
65
|
"skills",
|
|
66
|
+
"skill",
|
|
63
67
|
"skill-pack",
|
|
68
|
+
"dsh-bundle",
|
|
69
|
+
"gis-skills",
|
|
64
70
|
"gis",
|
|
65
71
|
"geospatial",
|
|
66
72
|
"opengis",
|
package/upstream.json
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
+
"schemaVersion": 1,
|
|
2
3
|
"repository": "https://github.com/znlgis/opengis-skills",
|
|
3
4
|
"commit": "c18c097b93a1e01792bdcc2d7c0f5c7f0f664988",
|
|
4
|
-
"syncedAt": "2026-10-
|
|
5
|
+
"syncedAt": "2026-10-19T04:05:40.659Z",
|
|
6
|
+
"hashAlgorithm": "sha256",
|
|
5
7
|
"skillCount": 32,
|
|
6
8
|
"skills": [
|
|
7
9
|
"cesiumjs",
|
|
@@ -36,5 +38,65 @@
|
|
|
36
38
|
"qgis-server",
|
|
37
39
|
"shapely",
|
|
38
40
|
"sharpmap"
|
|
39
|
-
]
|
|
41
|
+
],
|
|
42
|
+
"files": {
|
|
43
|
+
"cesiumjs/SKILL.md": "d15c900b0cb4eb3d9cbba9d64f3bb2abcf49c42bdf4cfdc07074c2536691fda0",
|
|
44
|
+
"deck-gl/SKILL.md": "fd41bdddf7bef5bae98c8ee06cf5fbb1d940edf7ca38c3187d17c05bb22027d6",
|
|
45
|
+
"gdal-api/SKILL.md": "6d6c573a0c815eb56a5ba37a825fabb24616babb3e1b957031bf52a8042b7cfe",
|
|
46
|
+
"gdal-api/reference/raster-api.md": "c0d4526602bf3e2e3f80abeaa972997c132e3f6fbbabd79347d035a3cd1f1c84",
|
|
47
|
+
"gdal-api/reference/vector-api.md": "5907be299d5e969f294c25a53737454f0e5abcd6d74e0d3831ddd46d9f18d03f",
|
|
48
|
+
"gdal/SKILL.md": "3db004f046ab3882a7136ecb093826b9eddb74f53e8a098e9cfcffdb889b772f",
|
|
49
|
+
"gdal/reference/data-formats.md": "443731a0f13330ab90e7ecc3e8e46d85519fbcd7013861dfa63f693668fdffd3",
|
|
50
|
+
"gdal/reference/raster-tools.md": "a3ed2dbb9571ce59973729e8cea5d68cf039d30bf39c98b614fb523a61541f97",
|
|
51
|
+
"gdal/reference/vector-tools.md": "284b5685f3f72b13865a8da2bef33637eb92820aa0b3cfec57f8c745a1fe3966",
|
|
52
|
+
"geometry-api-java/SKILL.md": "7b6d41dc8c4289b6c7744daba4dbb7b61afc80709bea42ec7def5b5855bc28a0",
|
|
53
|
+
"geometry-api-net/SKILL.md": "d53ff7d848524ce132b5100f7443561f285502addac5b42f8f03ae68ec2449cb",
|
|
54
|
+
"geometry-api-net/reference/geometry-types.md": "121e6ef0f5c7fb053b8e14bf42e952629bfb8b0717626fd7744db59ca6cfb4e7",
|
|
55
|
+
"geopandas/SKILL.md": "26f00dea35b0a9a35ce9b0580b38ff4684ffeb64156b96437aec4e5d9e14db42",
|
|
56
|
+
"geopandas/reference/core-api.md": "1e233adb18510d7d4788cfc18ac47982b53784b09ce37d2171494e9beba4e05f",
|
|
57
|
+
"geopipe-agent/SKILL.md": "50c788dcb87259b1a1102d7c4aee7fc36c44bfef3b73029c84a650d693d100c3",
|
|
58
|
+
"geopipe-agent/reference/backends.md": "00d7658470e67661d6774eb827f9ab0ccbb2262604b3cb65a0382bf27d2b331f",
|
|
59
|
+
"geopipe-agent/reference/knowledge.md": "63b1d44d6f83d4e33011c35855d08b2c83cd07f2ec21722a120b1e2164685e07",
|
|
60
|
+
"geopipe-agent/reference/pipeline-schema.md": "9b6a7a87002fa2fa53f61441a0f9ace804b970ba9ea6b00b0308e2dba9a77cb6",
|
|
61
|
+
"geopipe-agent/reference/self-extension.md": "552be0bfaef58f3cb7f10567362c3c7328bea520b8ef61bda5f79f84529e16a1",
|
|
62
|
+
"geopipe-agent/reference/steps-reference.md": "3c1adaf5f9960ea9c99919cc61bb065e59ce2b77a6a029aa2a2e9977dc794bf8",
|
|
63
|
+
"geopipe-agent/reference/workflows.md": "e4e0fb32dd83b7f7b27de5a42f6a2d7eea71c4fae7e8c7764d7defe7340e1313",
|
|
64
|
+
"geoserver-cloud/SKILL.md": "f7943b645440f03bab9055201a7552b7e86be47c8ce27b5f1c57487107e4300a",
|
|
65
|
+
"geoserver-rest-api/SKILL.md": "64827e5b164687a2b7e44f668a30f52f6ea66bd205a953e03b7357a3db7427d2",
|
|
66
|
+
"geoserver-rest-api/reference/endpoints.md": "20fbb2ea7ccddcbb34f5ecb227d53127073a8109edbff9ad1bbcae07a3cf1be7",
|
|
67
|
+
"geoserver/SKILL.md": "0e266a7975d80c7facda8dac8d1bce3e519fca11afa2bef631b10f8023769158",
|
|
68
|
+
"geotools/SKILL.md": "1bf76ab5870adba3ca4faca7887b2081e64bd04cdb6f5154ad08f14002337d3a",
|
|
69
|
+
"geotools/reference/raster-rendering.md": "3b1876ad39d8c11756b1ff5eac8cda9dab0d94f958f8cafa4f32d9ff5fa6ebd3",
|
|
70
|
+
"geotools/reference/vector-io.md": "2dca5de4e149bc9e79bd5bfcb345c36e45da06a9d86a53bc28869168f55902f1",
|
|
71
|
+
"grass-gis/SKILL.md": "fc908f4bf14a026cf41997415f4a7d6d43b94914d1062e31f86c133dbc7343bc",
|
|
72
|
+
"jts/SKILL.md": "a8b739bc67e65f11cc714939e7e13e98736246693f61e07a662b1470b468888e",
|
|
73
|
+
"jts/reference/geometry-operations.md": "a242f1f0185136baa60f632072c041fb734185b844da25c37ef1f4e3b2685124",
|
|
74
|
+
"leaflet/SKILL.md": "44238568e0682acb72bfd68d97f25fb75608e9e2683965abfb272ba0d5bd46da",
|
|
75
|
+
"mapbox-gl-js/SKILL.md": "6c10bf3f7c3968f41f3bc030c1ca0c49f51fcea2b4c42a4f347cf845f9845759",
|
|
76
|
+
"mapserver/SKILL.md": "df291706233d90be652405726740667f2b6d82eee6594dcadd7979daa4e15aaf",
|
|
77
|
+
"mapsui/SKILL.md": "1a9eac49a40477360348e9dc37ed3cde9143b6f7e7feeef6838c12c0578f213e",
|
|
78
|
+
"nettopologysuite/SKILL.md": "a045193f0e4a16a9876fc36909b256eeb93901c14c77db3c914a8f8e24c514f0",
|
|
79
|
+
"opengis-all/SKILL.md": "e4b31d462be1c52fed6b019bb45321be4a40723a0b0bb28f4e6f3631e0175ef4",
|
|
80
|
+
"opengis-all/reference/data-stages.md": "88d0182d8a96469b0ea8857a463517b3b38c1fde72b5599cad3958da7c8cb4ed",
|
|
81
|
+
"opengis-all/reference/workflow-cheatsheets.md": "3bb58ea3b966ab1bd85380cda1253056ff03accb97f37d863f3f96e7ce6dbdb7",
|
|
82
|
+
"opengis-daf/SKILL.md": "3d8c496ecb0c54cc837f83355a67ba0866787ba58ef151c077b6c40fe014c040",
|
|
83
|
+
"opengis-utils-for-java/SKILL.md": "bb883b9175ffafd649eda066cffe642a0bebe6d29cf6d03b389d71b1a97259a6",
|
|
84
|
+
"opengis-utils-for-java/reference/common-tasks.md": "a32f2e1181d9c49e1c8cc16bba1345c3d8aca6d42be1cc2afe2a330bd382b47a",
|
|
85
|
+
"opengis-utils-for-net/SKILL.md": "d0e63e540ec731904551ad5ffdaa8374430ea85e7439d88126102b32b78be560",
|
|
86
|
+
"opengis-utils-for-net/reference/code-patterns.md": "379a6a22e10b291639fa6ee07ad105522523420666ea52a5e588f19fc5c8a064",
|
|
87
|
+
"opengis-utils-for-net/reference/util-classes.md": "111fc1347edb8af6660b0f9614b5ecff4b6654ea19fe1549f6924eb8b3467db2",
|
|
88
|
+
"openlayers/SKILL.md": "6cfe6f10cc69e9e36b8db8e1a2896553144d6346061bdc433fbcf9dbc3ebe96e",
|
|
89
|
+
"pdal/SKILL.md": "eb4733ad80540b9911ca0a7aa6600bb0647f32dd088693c6f8855a0a45bff90d",
|
|
90
|
+
"postgis/SKILL.md": "2f83487427263b54217d49c08ffc9ce362867c6344e3e789e3246e6cc673c621",
|
|
91
|
+
"proj/SKILL.md": "42d1cb70c732f27fb1ec687737759278a70fa73e6bca97a0b2cff641ee16c0d9",
|
|
92
|
+
"pyqgis/SKILL.md": "1d24d2072aeaa519374dca7cca0bd75de91e187f882ac460f6340eaee4cd6a79",
|
|
93
|
+
"pyqgis/reference/processing-rendering.md": "138644c36ca3f30e746016bb7457fd3a2d18349c5740f02d1735ba6c59173a3e",
|
|
94
|
+
"pyqgis/reference/vector-raster-layers.md": "fec965918eff2d24c6e8f8c68379c52bb10727892f8d599e6c425760d168cd2d",
|
|
95
|
+
"qgis-process/SKILL.md": "d9613cf47ae041c079b24d453074e6c4f3b5477617c16e9024aca3356f751aff",
|
|
96
|
+
"qgis-process/reference/algorithm-cheatsheet.md": "d88720d80a2d78efefcf1ff9027b451f291bef5ef166fa9befcae484bfa91e8f",
|
|
97
|
+
"qgis-server/SKILL.md": "2cd0aa5b4a6b4372f96c88318cd36c035c26783a8b6f5a77fb5dc0752367affa",
|
|
98
|
+
"shapely/SKILL.md": "8cc848b6ca28d2dd50ff951ca44ae90cd0255a33f5e329a62f31828947006fb0",
|
|
99
|
+
"shapely/reference/operations-reference.md": "a984e9cd84dbfa3bc299284725639520883a85289f67a61d7b296a00665c2714",
|
|
100
|
+
"sharpmap/SKILL.md": "4bf964bcbd663009982e4cbe06ea80b9c8ee626bfcf7b98b615612f9356a7f72"
|
|
101
|
+
}
|
|
40
102
|
}
|