dsh-arcgispro 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +196 -2
- package/commands/arcgis-check.md +13 -0
- package/cordis.patch.yml +5 -0
- package/icon.svg +26 -0
- package/index.js +3 -0
- package/lib/bridge.js +292 -0
- package/lib/index.js +234 -0
- package/lib/paths.js +145 -0
- package/lib/tool-kit.js +116 -0
- package/lib/tools/analysis.js +480 -0
- package/lib/tools/data.js +481 -0
- package/lib/tools/gp.js +161 -0
- package/lib/tools/index.js +25 -0
- package/lib/tools/layer.js +432 -0
- package/lib/tools/project.js +655 -0
- package/lib/tools/status.js +127 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -4
- package/py/arcutil.py +233 -0
- package/py/bridge.py +171 -0
- package/py/ops_analysis.py +792 -0
- package/py/ops_core.py +548 -0
- package/py/ops_data.py +708 -0
- package/py/ops_layer.py +566 -0
- package/py/ops_project.py +1252 -0
- package/py/ops_python.py +76 -0
- package/skills/arcgispro/SKILL.md +218 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 haiwenxiang061-dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,197 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-arcgispro
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://github.com/topics/dsh-plugin)
|
|
5
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
|
+
|
|
7
|
+
让 DeepSeek Harness 直接操作 ArcGIS Pro:1977 个 GP 工具、`.aprx` 工程、地图、图层、布局出图、要素类读写、许可与环境变量。
|
|
8
|
+
|
|
9
|
+
## 为什么是一个常驻 Python 桥
|
|
10
|
+
|
|
11
|
+
`import arcpy` 实测 **7–20 秒**(冷启动 20.6s,warm 7.2s),而之后任何一次调用都是 5–40ms。
|
|
12
|
+
所以这里不采用「每次执行都 spawn 一个 python」的做法——那样每个工具调用都要付一次 import 成本。
|
|
13
|
+
插件启动一个常驻进程,用 JSON Lines 通信,第一次工具调用付一次 import 成本,之后全走热进程。
|
|
14
|
+
|
|
15
|
+
桥把 **fd1 重定向为协议通道**(`os.dup2(2, 1)`),`sys.stdout` 指向 stderr。
|
|
16
|
+
原因:GP 工具会往 stdout 打印中文进度消息,混进协议流就会毁掉 JSON 解析。现在所有 GP 打印都进 stderr,只有协议 JSON 走 fd1。
|
|
17
|
+
|
|
18
|
+
## 安装
|
|
19
|
+
|
|
20
|
+
### 方式一:从 npm 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
dsh plugin --profile web add dsh-arcgispro
|
|
24
|
+
dsh plugin --profile desktop add dsh-arcgispro
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`desktop` 是带窗口的 ArcGIS Pro 桌面 profile,`web` 是浏览器 profile;把 profile 名换成你实际要装的那个。
|
|
28
|
+
装完刷新页面即生效,插件出现在「设置 → 插件」里,行内标注 **ArcGIS Pro 桥**。
|
|
29
|
+
需要宿主里已有 `plugin_manager`(`dsh-base` 自带)。
|
|
30
|
+
|
|
31
|
+
> 该包当前**尚未发布到 npm**,上面的命令会解析失败。现在要装请用方式二。
|
|
32
|
+
|
|
33
|
+
### 方式二:从 GitHub 安装(现在可用)
|
|
34
|
+
|
|
35
|
+
钉住 tag 的 tarball 地址,下载后直接解包安装,不需要 git 传输:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
dsh plugin --profile web add https://codeload.github.com/haiwenxiang061-dev/dsh-arcgispro/tar.gz/refs/tags/v0.1.0
|
|
39
|
+
dsh plugin --profile desktop add https://codeload.github.com/haiwenxiang061-dev/dsh-arcgispro/tar.gz/refs/tags/v0.1.0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Release 里也挂了同一份 `.tgz` 附件,想用 GitHub 的下载页可以换成:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
dsh plugin --profile desktop add https://github.com/haiwenxiang061-dev/dsh-arcgispro/releases/download/v0.1.0/dsh-arcgispro-0.1.0.tgz
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
两点实测注意:
|
|
49
|
+
- `dsh plugin` 会把参数转发给 profile 目录里的 pnpm,所以本机需要能调到 `pnpm`(`npm i -g pnpm` 即可)。
|
|
50
|
+
- 不建议用 `github:haiwenxiang061-dev/dsh-arcgispro` 这种简写:它走 git 传输协议,在国内网络下经常被 reset;上面的 codeload 地址走 HTTPS 下载,稳定得多。
|
|
51
|
+
|
|
52
|
+
### 源码链接安装(开发用)
|
|
53
|
+
|
|
54
|
+
profile 目录 `C:\Users\下雪了\.dsh\profiles\desktop\`:
|
|
55
|
+
|
|
56
|
+
```jsonc
|
|
57
|
+
// package.json
|
|
58
|
+
"dependencies": { "dsh-arcgispro": "file:../../../workplace/dsh-arcgispro" },
|
|
59
|
+
"dsh": { "profile": { "bundles": [ /* ... */ "dsh-arcgispro" ] } }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`node_modules/dsh-arcgispro` 指向源码目录即可(junction 或 symlink 都行)。
|
|
63
|
+
|
|
64
|
+
Python 定位顺序:配置项 `python` → 环境变量 `DSH_ARCGISPRO_PYTHON` → 注册表 `HKLM\SOFTWARE\ESRI\ArcGISPro` 的 `InstallDir` → 常见安装根目录。
|
|
65
|
+
默认环境是 `<InstallDir>\bin\Python\envs\arcgispro-py3\python.exe`。
|
|
66
|
+
|
|
67
|
+
## 工具(87 个)
|
|
68
|
+
|
|
69
|
+
**状态与许可**
|
|
70
|
+
`arcgis_status`(含 `restart`)· `arcgis_license`(info/checkout/checkin/setProduct)· `arcgis_environment`(读写 74 项 GP 环境变量)
|
|
71
|
+
|
|
72
|
+
**GP 工具目录**
|
|
73
|
+
`arcgis_tool_list` · `arcgis_tool_info` · `arcgis_tool_run`(`params` 按名字 / `args` 按位置二选一)
|
|
74
|
+
|
|
75
|
+
**任意 arcpy 代码**
|
|
76
|
+
`arcgis_python`——在桥进程里执行 Python,命名空间里已备好 `arcpy`、`arcutil` 和当前工程对象 `aprx`,脚本的 `result` 变量与 stdout/stderr 一并带回。
|
|
77
|
+
所有工具都盖不住的能力(`arcpy.da` 游标、`arcpy.metadata`、CIM 深度定制、第三方扩展)从这里进;要求 `writeAllowed: true`,无参数校验、无回滚。
|
|
78
|
+
|
|
79
|
+
**工程 / 地图 / 图层 / 布局**
|
|
80
|
+
`arcgis_project_open` · `arcgis_project_info` · `arcgis_project_new` · `arcgis_project_save` · `arcgis_project_close` · `arcgis_project_broken_sources`
|
|
81
|
+
`arcgis_map_new` · `arcgis_map_info` · `arcgis_map_add_data` · `arcgis_map_add_basemap` · `arcgis_map_remove_layer` · `arcgis_map_export` · `arcgis_camera_set`
|
|
82
|
+
`arcgis_layer_info` · `arcgis_layer_update` · `arcgis_layer_symbology`
|
|
83
|
+
`arcgis_layout_list` · `arcgis_layout_info` · `arcgis_layout_new` · `arcgis_layout_page_size` · `arcgis_layout_add_frame` · `arcgis_layout_elements` · `arcgis_layout_text_set` · `arcgis_layout_export`
|
|
84
|
+
|
|
85
|
+
**地图系列**
|
|
86
|
+
`arcgis_mapseries_info` · `arcgis_mapseries_create` · `arcgis_mapseries_export`
|
|
87
|
+
|
|
88
|
+
**选择**
|
|
89
|
+
`arcgis_select_by_attribute` · `arcgis_select_by_location` · `arcgis_select_clear`
|
|
90
|
+
|
|
91
|
+
**数据**
|
|
92
|
+
`arcgis_data_describe` · `arcgis_data_list` · `arcgis_data_fields` · `arcgis_data_query` · `arcgis_data_statistics`
|
|
93
|
+
`arcgis_data_insert` · `arcgis_data_update` · `arcgis_data_delete` · `arcgis_data_calculate_field`
|
|
94
|
+
`arcgis_data_create_featureclass` · `arcgis_data_create_gdb` · `arcgis_data_create_table` · `arcgis_data_copy`
|
|
95
|
+
`arcgis_data_add_field` · `arcgis_data_delete_field` · `arcgis_data_merge` · `arcgis_data_append` · `arcgis_data_dissolve`
|
|
96
|
+
|
|
97
|
+
**投影与格式转换**
|
|
98
|
+
`arcgis_reproject`(矢量与栅格自动分流)· `arcgis_define_projection` · `arcgis_convert_data`(shapefile / json / excel)
|
|
99
|
+
|
|
100
|
+
**栅格**
|
|
101
|
+
`arcgis_raster_info` · `arcgis_raster_resample` · `arcgis_raster_clip` · `arcgis_raster_mosaic`
|
|
102
|
+
`arcgis_raster_calculator` · `arcgis_raster_extract_mask` · `arcgis_raster_surface`(slope/aspect/hillshade/contour)
|
|
103
|
+
|
|
104
|
+
**图层级细操作**
|
|
105
|
+
`arcgis_basemap_list` · `arcgis_layer_add`(.lyrx)· `arcgis_layer_move` · `arcgis_layer_group` · `arcgis_layer_toggle` · `arcgis_layer_export`
|
|
106
|
+
`arcgis_layer_symbology_apply` · `arcgis_label_info` · `arcgis_label_set`(Arcade 表达式)
|
|
107
|
+
`arcgis_table_list` · `arcgis_table_add` · `arcgis_style_list` · `arcgis_colorramp_list` · `arcgis_report_list`
|
|
108
|
+
|
|
109
|
+
**书签与地图文档**
|
|
110
|
+
`arcgis_bookmark_list` · `arcgis_bookmark_create` · `arcgis_bookmark_remove` · `arcgis_bookmark_export` · `arcgis_bookmark_import`
|
|
111
|
+
`arcgis_map_definition`(CIM)· `arcgis_map_color_model` · `arcgis_map_reference_scale`
|
|
112
|
+
|
|
113
|
+
## 典型流程
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
arcgis_tool_info analysis.Buffer # 先看参数契约,params 的键就是这里的 name
|
|
117
|
+
arcgis_tool_run analysis.Buffer # params: {in_features, out_feature_class, buffer_distance_or_field}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
arcgis_data_create_gdb → arcgis_data_create_featureclass → arcgis_data_insert
|
|
122
|
+
arcgis_data_query → 几何字段回 {wkt, type, area, length}
|
|
123
|
+
arcgis_tool_run analysis.Buffer → 输出要素类落地
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
arcgis_project_open → arcgis_layer_update(定义查询/可见性)→ arcgis_project_save
|
|
128
|
+
→ arcgis_layout_export(带图例比例尺的正式出图)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 三条设计约束
|
|
132
|
+
|
|
133
|
+
**改动只在内存,`project_save` 才落盘。** 图层可见性、符号、定义查询、加删数据都作用于内存工程对象。这样一系列操作要么整体保存,要么整体丢弃,不会出现半个状态写进 `.aprx`。
|
|
134
|
+
|
|
135
|
+
**调用串行。** arcpy 不是线程安全的:`arcpy.env.*`、当前工作空间、许可检出状态、GP 消息缓冲全是进程级全局。桥内部用队列串行化,并发调用会排队而不是互相污染。
|
|
136
|
+
|
|
137
|
+
**写操作要显式 `writeAllowed: true`。** 磁盘改动(写工程、改数据、删字段、导出文件)必须显式授权,缺了直接拒绝。`arcgis_data_delete` 另有一层:空 `where` 必须再给 `allowAll: true`。
|
|
138
|
+
|
|
139
|
+
## 实测踩坑(已内建处理)
|
|
140
|
+
|
|
141
|
+
- **`da.SearchCursor` 传 `Shape` 只返回坐标元组**,必须传 `"SHAPE@"` 才拿几何对象。`arcgis_data_query` 内部按 `Describe().shapeFieldName` 自动换 token,输出字段名保持原样。
|
|
142
|
+
- **`ListFeatureClasses` 只接受 2 个位置参数**,工作空间必须走 `arcpy.env.workspace`;`arcgis_data_list` 临时设环境并在 `finally` 还原。
|
|
143
|
+
- **`os.path.exists` 对 gdb 内要素类永远假**(`x.gdb/点要素` 不是文件系统路径),存在性判断一律走 `arcpy.Exists`。
|
|
144
|
+
- **`Camera` 没有 `spatialReference`**,坐标系取地图对象的。
|
|
145
|
+
- **`SimpleRenderer` 没有 `groups`**,符号系统读取要按 `hasattr(renderer, "groups")` 分流。
|
|
146
|
+
- **`MapView.exportToPNG` 的宽高是必需位置参数**,`layout.exportToPDF` 不是。
|
|
147
|
+
- **GP 消息缓冲是全局残留**:一次失败调用后面会挂着上一条无关的「行计数 = 5」。桥在每次调用前先排空。
|
|
148
|
+
- **中文** 全程 UTF-8:路径、要素类名、字段名、属性值、GP 消息。桥进程固定 `PYTHONIOENCODING=utf-8`、`PYTHONUTF8=1`,否则 cp936 会把输出毁成乱码。
|
|
149
|
+
- **工程搬机器后图层数据源会断链**,`arcgis_map_info` 每个图层带 `dataSource` 与 `dataSourceMissing`,一眼看出哪个没了。
|
|
150
|
+
- **`createMap` 在 Pro 3.6 上极慢**:从模板克隆底图工程,实测首次 102–115 秒、再次 56 秒,与有没有网络无关,摘掉底图也省不掉。`arcgis_map_new` 建完会把默认的 `Topographic` 底图摘掉(离线时它必然是断链的),要底图单独调 `arcgis_map_add_basemap`。
|
|
151
|
+
- **`MapSeries` 只有 16 个成员**(`clipToIndexFeature / currentPageNumber / enabled / export / exportToPDF / getDefinition / getPageNumberFromName / indexLayer / mapFrame / pageCount / pageNameField / pageRow / refresh / selectedIndexFeatures / setDefinition`),没有 `type / nameField / sortField / startingPageNumber`;`getDefinition()` 必须传 `cim_version`。
|
|
152
|
+
- **栅格计算器的 `eval` 不能给空 `__builtins__`**:`arcpy.sa.Raster` 重载了算术运算,`pop * 2` 内部要 `__import__`,空内建直接 `KeyError: '__import__'`。只放计算必需的内建。
|
|
153
|
+
- **`SelectLayerByAttribute` 用 `NON_INVERT`,`SelectLayerByLocation` 用 `NOT_INVERT`**,两边不通用,写错报 `ERROR 000800`。
|
|
154
|
+
- **`Dissolve` 的 `statistics_fields` 只吃 `[[字段, 统计]]`**,`[{field, stat}]` 报一个指不到参数位置的 `Object: 执行工具时出错`。
|
|
155
|
+
- **GP 不建输出目录**,报的却是 `ERROR 000800: 参数无效`。桥在 `gp.execute` 前按 `GetParameterInfo` 的 `direction == Output` 准备好输出容器,输入路径不碰。
|
|
156
|
+
- **输出到还不存在的 `.gdb` 报 `ERROR 000210: 无法创建输出`**,而 gdb 不能靠 `makedirs` 造。桥检出路径里的 `.gdb` 就用 `CreateFileGDB` 建出来(父目录一并建)。
|
|
157
|
+
- **`arcgis_tool_run` 支持按位置传参**(`args: [...]`,顺序同 `arcgis_tool_info` 的 `index`),与 `params` 二选一。位置传参也会走同一套输出容器准备——早期版本直接 `fn(*args)` 绕过了它,于是同样的输出路径按名字传能成、按位置传就报 000800。
|
|
158
|
+
- **`listBasemaps()` / `styles` 返回字符串数组**,`listColorRamps()` / `listReports()` 返回对象。同一族接口两种形状,统一按 `isinstance(x, str)` 分流。
|
|
159
|
+
- **`Map.getDefinition("V3")` 返回 CIMMap**:既没有 `toJSON()` 也不是 dict,`arcpy` 的通用序列化会把它折成地图名字符串。真实结构只能按 `dir()` 的公开成员名取(实测 60+ 个:`layers / defaultCamera / referenceScale / colorModel / …`)。`arcgis_map_definition` 默认只回键名,`full: true` 才取全部值。
|
|
160
|
+
- **`LabelClass` 只有 6 个成员**(`SQLQuery / expression / getDefinition / setDefinition / name / visible`),字体字号偏移要走 CIM。要素图层默认**没有**标注类,`arcgis_label_set` 会自动 `createLabelClass` 补一个。
|
|
161
|
+
- **无窗口工程里分组没法填成员**:`addLayerToGroup(layer, group)` 抛 `ValueError`(内容被包成 `MappingLayerObject` 对象,等于没信息);`insertLayer(layer, group, "AFTER")` 返回成功但**每次克隆出一个新组**;组的 `setDefinition` 塞 `layers=[layerDef]` 也返回成功而组内仍为空。`arcgis_layer_group` 因此只保证"分组建出来",返回 `membered: false` + `note` 如实说明。
|
|
162
|
+
- **`Map` 没有 `createBookmark`**(只有 `copyBookmark`)。书签只能在有窗口的工程里从视图存;无窗口工程要书签就用 `arcgis_bookmark_import` 导入现成 json。空地图导出书签也会明确报「此地图中不存在书签」。
|
|
163
|
+
- **底图名不能想当然**:`addBasemap` 只认在线底图名(Topographic / Streets / Imagery…)。装了天地图插件的机器 `listBasemaps()` 给的是「天地图-地形(CGCS2000)」这类中文名,拿去 `addBasemap` 会被拒。先 `arcgis_basemap_list` 查,加不上时按报错换名字。
|
|
164
|
+
- **`Map.moveLayer` 的第一个参数是参照层,第二个才是要移动的层**:真实签名 `moveLayer(reference_layer, move_layer, insert_position='BEFORE')`。顺序写反**不报错**,但会把参照层挪走——症状是 `before` 看起来像 `after`(实测把 `school` 挪到 `Marketplace` 之前,结果变成 `Marketplace, school`)。`insert_position` 只认 `BEFORE`(参照层上方)/ `AFTER`(下方),与内容列表顺序一致。`reference_layer` 传 `None` 直接 `ValueError`(且没有信息),所以"置顶"要拿当前第一层当参照、目标插到它上面。
|
|
165
|
+
- **`Map.addTable()` 两个坑叠在一起**:必须传 `arcpy.mp.Table` 对象(传路径字符串只抛一句 `SystemError: ... returned a result with an exception set`,连 `GetMessages()` 都是空的);而 `Table()` 又只认**反斜杠**路径,传 `F:/x.gdb/表` 报 `SystemError: <class 'MappingTableObject'> returned NULL without setting an exception`。我们内部路径一律正斜杠,所以 `op_table_add` 里要 `source.replace("/", os.sep)`。
|
|
166
|
+
- **`removeLayer` 之后 Layer 对象就脱离了地图**,再读 `.name` 直接 `AttributeError: The attribute 'name' is not supported on this instance of Layer`。要报"移除了谁",名字得在移除**之前**读下来。
|
|
167
|
+
- **`arcpy.env` 的写入口收的是 `settings` 对象**(一次能写多项),不是 `name`/`value` 两个字段。工具层传错了会得到一句 `settings 必须是非空对象`,看起来像参数没给——这个错在 `all-tools.mjs` 里才被抓到(之前只测过读)。
|
|
168
|
+
|
|
169
|
+
## 兼容与权限
|
|
170
|
+
|
|
171
|
+
| 项 | 值 |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| DSH 版本 | 0.2.x,`dsh.bundle.patch` + 导出 `apply(ctx)` |
|
|
174
|
+
| 平台 | Windows(arcpy 与 ArcGIS Pro 仅 Windows 版) |
|
|
175
|
+
| 外部依赖 | 本机已安装 ArcGIS Pro 3.x;Python 解释器由插件按 `python` 配置 → `DSH_ARCGISPRO_PYTHON` → 注册表 → 常见安装路径自动定位 |
|
|
176
|
+
| 权限 | 启动一个常驻 Python 子进程执行 arcpy;写盘与改工程类工具(`arcgis_python`、`arcgis_data_*`、`arcgis_layout_export`、`arcgis_project_save` 等)要求 `writeAllowed: true`,缺省只读 |
|
|
177
|
+
| 网络 | 插件自身不联网;只有 ArcGIS 加载在线底图时由 Pro 联网 |
|
|
178
|
+
| 许可证 | MIT |
|
|
179
|
+
|
|
180
|
+
## 测试
|
|
181
|
+
```
|
|
182
|
+
node test/smoke.mjs # 工具定义契约,秒级
|
|
183
|
+
node test/all-tools.mjs # 87 个工具逐个真调一遍,约 4 分钟
|
|
184
|
+
node test/live.mjs --project <path.aprx> # 真桥实机,13-21 秒
|
|
185
|
+
node test/live-extended.mjs # 全能力链路,2-3 分钟(含 createMap 的 ~110s)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`all-tools.mjs` 是"有没有哪个工具一调就坏"的兜底:每条用例按顺序造数据、调工具,最后核对**注册的 87 个工具是否都至少成功调用过一次**,漏一个就退出码 1。Pro 本身做不到的事(无窗口工程里 `createBookmark`、空地图导出书签)用 `allowError` 标出来——只要错误说得清楚就算通过,测试要能区分"工具坏了"和"这件事本来就不行"。
|
|
189
|
+
|
|
190
|
+
`live.mjs` 覆盖:桥启动参数、工具目录检索、中文 gdb 往返、真实工程打开与布局导出、建库→建要素类→插中文点→回读→跑 Buffer、两条写盘闸门拒绝用例。
|
|
191
|
+
|
|
192
|
+
`live-extended.mjs` 覆盖:建库建表 → 插点 → 复制 → 属性/空间选择 → 投影与重定义 → 转 json → 合并 → 融合 → 点转栅格 → 重采样 → 掩膜裁剪 → 栅格计算 → 坡度 → 掩膜提取 → 镶嵌 → 新建工程/地图/加数据 → 布局新建/改页/加图框/列元素 → 地图系列建/查/逐页导出 → 存盘。断言只记账不抛出,一轮跑完能看到全部失败项。
|
|
193
|
+
它会往 `F:/workspace/_aprprobe_ext` 写测试数据(每次用唯一库名,不会撞 ERROR 000258)。
|
|
194
|
+
|
|
195
|
+
## 许可
|
|
196
|
+
|
|
197
|
+
MIT。详见 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 自检 ArcGIS Pro 环境:安装位置、Python 环境、许可、arcpy 桥、当前工程
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
跑一次完整自检,把诊断结果摊开给我看。按顺序做:
|
|
6
|
+
|
|
7
|
+
1. `arcgis_status` —— 一次拿到安装信息、Python 与 arcpy 版本、产品许可级别、可用扩展、桥的启动耗时。如果 `ok` 是 false,直接把 `error` 原文报出来,不要继续往下猜。
|
|
8
|
+
2. 检查返回里 `installation.pythonExists`。如果是 false,说明注册表指向的安装目录里没有 `arcgispro-py3`,告诉我实际找到的路径和应该设的 `DSH_ARCGISPRO_PYTHON`。
|
|
9
|
+
3. `arcgis_project_info` —— 看当前会话有没有已经打开的工程。有就报工程路径、地图数、布局数、每个地图的图层数和数据源缺失数;没有就明说"当前没有打开工程"。
|
|
10
|
+
4. 如果有打开的工程且存在数据源缺失的图层,列出那些图层名和它们的 `dataSource`——这类断链是工程迁移后最常见的问题。
|
|
11
|
+
5. `arcgis_environment`(不传 name,读全部)—— 只挑出 `workspace`、`overwriteOutput`、`outputCoordinateSystem`、`addOutputsToMap` 这几个报,其余不用漫天列。
|
|
12
|
+
|
|
13
|
+
最后给一段简短结论:能不能干活、缺什么、下一步该做什么。别把原始 JSON 直接贴给我。
|
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="ArcGIS Pro bridge">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="sea" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#0d3b5c"/>
|
|
5
|
+
<stop offset="1" stop-color="#12709f"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="land" x1="0" y1="0" x2="0" y2="1">
|
|
8
|
+
<stop offset="0" stop-color="#4ec9a5"/>
|
|
9
|
+
<stop offset="1" stop-color="#1f8f76"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
<rect x="1" y="1" width="94" height="94" rx="20" fill="url(#sea)"/>
|
|
13
|
+
<path d="M12 62 L30 40 L45 52 L58 32 L84 62 L84 78 Q84 84 78 84 L18 84 Q12 84 12 78 Z" fill="url(#land)" opacity="0.92"/>
|
|
14
|
+
<path d="M12 62 L30 40 L45 52 L58 32 L84 62" fill="none" stroke="#d9f7ee" stroke-width="2.4" stroke-linejoin="round" opacity="0.85"/>
|
|
15
|
+
<g stroke="#bfe9ff" stroke-width="1.2" opacity="0.45">
|
|
16
|
+
<path d="M24 84 V58"/><path d="M40 84 V66"/><path d="M56 84 V70"/><path d="M72 84 V66"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g transform="translate(70 22)">
|
|
19
|
+
<path d="M0 -9 L5 7 L0 3.6 L-5 7 Z" fill="#ffffff"/>
|
|
20
|
+
<path d="M0 -13 L0 -10" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
|
|
21
|
+
</g>
|
|
22
|
+
<g fill="none" stroke="#7fd8ff" stroke-width="2" opacity="0.6">
|
|
23
|
+
<rect x="14" y="14" width="26" height="18" rx="3"/>
|
|
24
|
+
<rect x="20" y="20" width="26" height="18" rx="3"/>
|
|
25
|
+
</g>
|
|
26
|
+
</svg>
|
package/index.js
ADDED
package/lib/bridge.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 常驻 arcpy 桥的 Node 侧客户端。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么要常驻
|
|
5
|
+
*
|
|
6
|
+
* 实测 Pro 3.6 / Python 3.13.7:`import arcpy` 一次 15-21 秒,之后每次 GP 调用
|
|
7
|
+
* 5-40 毫秒。若每个工具调用起一个新进程,"读一个字段"会从毫秒变半分钟。
|
|
8
|
+
* 所以桥进程活到宿主关闭为止,每次调用只写一行 JSON。
|
|
9
|
+
*
|
|
10
|
+
* ## 串行
|
|
11
|
+
*
|
|
12
|
+
* arcpy 不是线程安全的(`env.workspace`、当前许可、GP 消息缓冲全是进程级全局
|
|
13
|
+
* 状态),并发调用会互相污染。所有请求排成一条链,前一个 settle 再发下一个。
|
|
14
|
+
* 单个调用本身在 Python 侧同步执行,Node 侧排队不会额外增加延迟。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn } from 'node:child_process'
|
|
18
|
+
import { EventEmitter } from 'node:events'
|
|
19
|
+
|
|
20
|
+
/** 单次调用默认超时。GP 工具跑长任务很正常,给足 5 分钟。 */
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 300_000
|
|
22
|
+
|
|
23
|
+
/** 启动要等 `import arcpy`,实测 15-21 秒,留足余量。 */
|
|
24
|
+
const START_TIMEOUT_MS = 180_000
|
|
25
|
+
|
|
26
|
+
/** stderr 只留最近这么多行做诊断,arcpy 打印量可能很大。 */
|
|
27
|
+
const STDERR_TAIL = 80
|
|
28
|
+
|
|
29
|
+
export class ArcPyError extends Error {
|
|
30
|
+
constructor(payload) {
|
|
31
|
+
const info = payload?.error ?? {}
|
|
32
|
+
super(info.message || 'arcpy 调用失败')
|
|
33
|
+
this.name = 'ArcPyError'
|
|
34
|
+
this.type = info.type || 'Error'
|
|
35
|
+
this.op = payload?.op
|
|
36
|
+
this.messages = info.messages || ''
|
|
37
|
+
this.traceback = info.traceback || ''
|
|
38
|
+
this.elapsedMs = payload?.elapsedMs
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 拼成一段模型能直接读懂的错误描述。 */
|
|
42
|
+
toText() {
|
|
43
|
+
const parts = [`${this.type}: ${this.message}`]
|
|
44
|
+
if (this.messages && this.messages.trim() !== '') parts.push(`GP 消息:\n${this.messages.trim()}`)
|
|
45
|
+
return parts.join('\n')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ArcPyBridge extends EventEmitter {
|
|
50
|
+
#proc = null
|
|
51
|
+
#closed = false
|
|
52
|
+
#starting = null
|
|
53
|
+
#bootInfo = null
|
|
54
|
+
#nextId = 1
|
|
55
|
+
#pending = new Map()
|
|
56
|
+
#queue = Promise.resolve()
|
|
57
|
+
#buffer = ''
|
|
58
|
+
#stderr = []
|
|
59
|
+
#python
|
|
60
|
+
#script
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {{python: string, script: string, cwd?: string}} options
|
|
64
|
+
* python — arcgispro-py3 的 python.exe
|
|
65
|
+
* script — bridge.py 的绝对路径
|
|
66
|
+
*/
|
|
67
|
+
constructor(options) {
|
|
68
|
+
super()
|
|
69
|
+
this.#python = options.python
|
|
70
|
+
this.#script = options.script
|
|
71
|
+
this.cwd = options.cwd
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get running() {
|
|
75
|
+
return this.#proc !== null && !this.#closed
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get bootInfo() {
|
|
79
|
+
return this.#bootInfo
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 最近的一段 stderr,用于把 arcpy 的底层抱怨暴露给模型。 */
|
|
83
|
+
get stderrTail() {
|
|
84
|
+
return this.#stderr.join('')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async start() {
|
|
88
|
+
if (this.#closed) {
|
|
89
|
+
// 之前 stop 过,允许重新拉起
|
|
90
|
+
this.#closed = false
|
|
91
|
+
}
|
|
92
|
+
if (this.running && this.#bootInfo) return this.#bootInfo
|
|
93
|
+
if (this.#starting) return this.#starting
|
|
94
|
+
this.#starting = this.#spawnAndBoot()
|
|
95
|
+
try {
|
|
96
|
+
this.#bootInfo = await this.#starting
|
|
97
|
+
return this.#bootInfo
|
|
98
|
+
} finally {
|
|
99
|
+
this.#starting = null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async #spawnAndBoot() {
|
|
104
|
+
const proc = spawn(this.#python, [this.#script], {
|
|
105
|
+
cwd: this.cwd,
|
|
106
|
+
// windowsHide:不弹黑框。桥是纯 stdio 的,不需要控制台。
|
|
107
|
+
windowsHide: true,
|
|
108
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
109
|
+
env: {
|
|
110
|
+
...process.env,
|
|
111
|
+
// 桥自己会 reconfigure,但这两个能让 arcpy 早期打印也用 UTF-8。
|
|
112
|
+
// 中文路径和中文属性值在 cp936 下会变成 Ҫ��ͼ�� 这种乱码。
|
|
113
|
+
PYTHONIOENCODING: 'utf-8',
|
|
114
|
+
PYTHONUTF8: '1',
|
|
115
|
+
PYTHONUNBUFFERED: '1',
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
this.#proc = proc
|
|
120
|
+
this.#buffer = ''
|
|
121
|
+
this.#stderr = []
|
|
122
|
+
|
|
123
|
+
proc.stdout.setEncoding('utf8')
|
|
124
|
+
proc.stdout.on('data', (chunk) => this.#onStdout(chunk))
|
|
125
|
+
proc.stderr.setEncoding('utf8')
|
|
126
|
+
proc.stderr.on('data', (chunk) => this.#onStderr(chunk))
|
|
127
|
+
|
|
128
|
+
proc.on('error', (error) => {
|
|
129
|
+
this.#failAll(new Error(`无法启动 arcpy 桥(${this.#python}):${error.message}`))
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
proc.on('exit', (code, signal) => {
|
|
133
|
+
const wasRunning = this.#proc === proc
|
|
134
|
+
this.#proc = null
|
|
135
|
+
if (!wasRunning) return
|
|
136
|
+
const detail = signal ? `signal ${signal}` : `exit code ${code}`
|
|
137
|
+
const tail = this.#stderr.join('').trim()
|
|
138
|
+
this.#failAll(
|
|
139
|
+
new Error(`arcpy 桥已退出(${detail})。${tail ? `\nstderr:\n${tail.slice(-2000)}` : ''}`),
|
|
140
|
+
)
|
|
141
|
+
this.emit('exit', { code, signal })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// 启动握手:boot 会返回 python/arcpy 版本和操作表大小。
|
|
145
|
+
const info = await this.#transmit('boot', {}, START_TIMEOUT_MS)
|
|
146
|
+
return info
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#onStdout(chunk) {
|
|
150
|
+
this.#buffer += chunk
|
|
151
|
+
let index = this.#buffer.indexOf('\n')
|
|
152
|
+
while (index >= 0) {
|
|
153
|
+
const line = this.#buffer.slice(0, index)
|
|
154
|
+
this.#buffer = this.#buffer.slice(index + 1)
|
|
155
|
+
this.#onLine(line)
|
|
156
|
+
index = this.#buffer.indexOf('\n')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#onLine(line) {
|
|
161
|
+
const text = line.trim()
|
|
162
|
+
if (text === '') return
|
|
163
|
+
let payload
|
|
164
|
+
try {
|
|
165
|
+
payload = JSON.parse(text)
|
|
166
|
+
} catch {
|
|
167
|
+
// 协议流里出现非 JSON,说明有东西绕过了 stdout 重定向。
|
|
168
|
+
// 不抛错——把原文塞进 stderr 尾部,最后会用得上。
|
|
169
|
+
this.#pushStderr(`[bridge 非协议输出] ${text}\n`)
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
const id = payload?.id == null ? null : String(payload.id)
|
|
173
|
+
if (id === null) return
|
|
174
|
+
const entry = this.#pending.get(id)
|
|
175
|
+
if (!entry) return
|
|
176
|
+
this.#pending.delete(id)
|
|
177
|
+
clearTimeout(entry.timer)
|
|
178
|
+
if (payload.ok === false) {
|
|
179
|
+
entry.reject(new ArcPyError({ ...payload, op: entry.op }))
|
|
180
|
+
} else {
|
|
181
|
+
entry.resolve(payload.result)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#onStderr(chunk) {
|
|
186
|
+
this.#pushStderr(chunk)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#pushStderr(text) {
|
|
190
|
+
this.#stderr.push(text)
|
|
191
|
+
// 按行裁剪,不按字节——半行日志切断了反而误导。
|
|
192
|
+
if (this.#stderr.length > STDERR_TAIL * 4) {
|
|
193
|
+
this.#stderr = this.#stderr.slice(-STDERR_TAIL * 2)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#failAll(error) {
|
|
198
|
+
for (const [, entry] of this.#pending) {
|
|
199
|
+
clearTimeout(entry.timer)
|
|
200
|
+
entry.reject(error)
|
|
201
|
+
}
|
|
202
|
+
this.#pending.clear()
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 调一次桥。返回 result(失败抛 ArcPyError)。
|
|
207
|
+
* 用 opts.timeoutMs 覆盖默认超时。
|
|
208
|
+
*/
|
|
209
|
+
async call(op, params = {}, opts = {}) {
|
|
210
|
+
if (!this.running) await this.start()
|
|
211
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
212
|
+
const run = () => this.#transmit(op, params, timeoutMs)
|
|
213
|
+
// 串行:把这次调用接在队尾,无论前一个成功还是失败都继续。
|
|
214
|
+
const settled = this.#queue.then(run, run)
|
|
215
|
+
this.#queue = settled.then(
|
|
216
|
+
() => undefined,
|
|
217
|
+
() => undefined,
|
|
218
|
+
)
|
|
219
|
+
return settled
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#transmit(op, params, timeoutMs) {
|
|
223
|
+
if (!this.running || this.#proc === null) {
|
|
224
|
+
return Promise.reject(new Error('arcpy 桥未运行。'))
|
|
225
|
+
}
|
|
226
|
+
const id = String(this.#nextId++)
|
|
227
|
+
const request = JSON.stringify({ id, op, params: params ?? {} })
|
|
228
|
+
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const timer = setTimeout(() => {
|
|
231
|
+
this.#pending.delete(id)
|
|
232
|
+
reject(
|
|
233
|
+
new Error(
|
|
234
|
+
`arcpy 调用超时(${Math.round(timeoutMs / 1000)}s):${op}。` +
|
|
235
|
+
'长任务可以调大 timeoutMs;若工具本身卡住,先 arcgis_status 看桥是否还活着。',
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
}, timeoutMs)
|
|
239
|
+
timer.unref?.()
|
|
240
|
+
this.#pending.set(id, { resolve, reject, timer, op })
|
|
241
|
+
try {
|
|
242
|
+
this.#proc.stdin.write(request + '\n', 'utf8')
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.#pending.delete(id)
|
|
245
|
+
clearTimeout(timer)
|
|
246
|
+
reject(new Error(`向 arcpy 桥写入请求失败:${error.message}`))
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 优雅关闭:先让 Python 侧退出循环,超时再强杀。 */
|
|
252
|
+
async stop() {
|
|
253
|
+
const proc = this.#proc
|
|
254
|
+
if (!proc) {
|
|
255
|
+
this.#closed = true
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
this.#closed = true
|
|
259
|
+
const exited = new Promise((resolve) => proc.once('exit', resolve))
|
|
260
|
+
try {
|
|
261
|
+
proc.stdin.write(JSON.stringify({ id: 'shutdown', op: 'shutdown' }) + '\n', 'utf8')
|
|
262
|
+
} catch {
|
|
263
|
+
// stdin 已关,直接走 kill
|
|
264
|
+
}
|
|
265
|
+
const timer = setTimeout(() => {
|
|
266
|
+
try {
|
|
267
|
+
proc.kill()
|
|
268
|
+
} catch {
|
|
269
|
+
// 已经死了
|
|
270
|
+
}
|
|
271
|
+
}, 3000)
|
|
272
|
+
timer.unref?.()
|
|
273
|
+
await exited
|
|
274
|
+
clearTimeout(timer)
|
|
275
|
+
this.#proc = null
|
|
276
|
+
this.#bootInfo = null
|
|
277
|
+
this.#failAll(new Error('arcpy 桥已关闭。'))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** 诊断快照:给 arcgis_status 用。 */
|
|
281
|
+
snapshot() {
|
|
282
|
+
return {
|
|
283
|
+
running: this.running,
|
|
284
|
+
pending: this.#pending.size,
|
|
285
|
+
queueDepth: this.#pending.size,
|
|
286
|
+
python: this.#python,
|
|
287
|
+
script: this.#script,
|
|
288
|
+
boot: this.#bootInfo,
|
|
289
|
+
stderrTail: this.stderrTail.trim().split('\n').slice(-20).join('\n'),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|