---
title: vjmapservice 文档单页版
source: https://vjmap.com/guide/service/README.html
---

# vjmapservice 文档单页版

> vjmap 后端 REST 接口文档 — 面向**二次开发者**的版本（`vjmapservice/`）。

后端SDK下载` python`、`nodejs`、`swagger`服务。下载地址：http://vjmap.com/download/vjmapservice.zip

![image-20260425205121562](https://vjmap.com/blogimages/202604252051713.png)

## 目录
- [00-overview.md](#00-overviewmd)
- [01-getting-started.md](#01-getting-startedmd)
- [02-conventions.md](#02-conventionsmd)
- [03-authentication.md](#03-authenticationmd)
- [04-workspace.md](#04-workspacemd)
- [api/index.md](#apiindexmd)
- [api/01-map-management.md](#api01-map-managementmd)
- [api/02-tiles-styles.md](#api02-tiles-stylesmd)
- [api/03-query.md](#api03-querymd)
- [api/04-analysis.md](#api04-analysismd)
- [api/05-compose-export.md](#api05-compose-exportmd)
- [api/06-webcad.md](#api06-webcadmd)
- [api/07-workspace.md](#api07-workspacemd)
- [api/08-data-storage.md](#api08-data-storagemd)
- [api/09-fullsearch.md](#api09-fullsearchmd)
- [api/10-symbols.md](#api10-symbolsmd)
- [api/11-system.md](#api11-systemmd)
- [api/12-service-misc.md](#api12-service-miscmd)
- [api/13-ai.md](#api13-aimd)

---

## 00-overview.md
<a id=00-overviewmd></a>

# 概述（Overview）



## 1. 产品定位

vjmap 是一套**以 CAD/GIS 图档为中心**的服务端系统，核心能力包括：

- 上传 DWG / DXF / 图片等源文件，在服务端解析成数据库
- 以**栅格瓦片**或 **MVT 矢量瓦片**的形式提供 Web 地图访问
- 基于 **cmd 指令**完成元数据查询、样式切换、几何分析、表格/中心线提取、文件导出等
- 存储用户 KV 数据、工作区、符号库、全文索引等辅助能力
- 支持 **Python / Node.js SDK** 直接对接；

典型使用场景：

- 将单位内部 DWG 档案转成 Web 可浏览地图
- 需要把 CAD 图与 WMS / MVT 图层混合使用
- 针对 DWG 做图形分析（比较、表格提取、中心线）并导出 PDF / DXF
- 在业务系统里存储轻量 KV 配置或自定义属性

## 2. 系统架构

```
┌────────────────┐   JWT      ┌──────────────────────┐   stdin/stdout   ┌──────────────────────────┐
│ Client (Web/   │ ─────────► │ web服务       │ ───────────────► │ 图形引擎服务     │
│ Python / Node) │            │ :27660               │                  │ geom / render / export    │
└────────────────┘ ◄───────── │ router + handler  │ ◄─────────────── │ appCmd / mapPsCmd / ...   │
        ▲                     │ + 缓存 / 限流 / 权限 │                  │  持久化        │
        │ REST / HTTP         └──────────────────────┘                  └──────────────────────────┘
        │                              │
        │                              ├── Workspace（多租户命名空间）
        │                              ├── Full-text search
        │                              ├── 符号库
        │                              └── AI / 向量检索（可选）
        │
        └── Tile 通过 CDN / HTTP 缓存直接给前端
```

- **web服务**（：入口；做 JWT 鉴权、路由转发、缓存清理、工作区路由改写。
- **图形服务 **：实际的 CAD 解析/渲染/分析。
- **数据存储**：每张图对应一个 数据库文件（默认 `data/maps/{hash}.db`），元数据、样式、缓存瓦片都放在里面；另有全局 KV、工作区、符号库等独立存储。
- **可选 AI**服务：`/ai/*`、`/aimcp/*`、`/aiagent/*` 三套子服务通过反代挂载；不是所有部署都启用。

## 3. 常用接口分 13 大类

| # | 分类 | 典型接口 | 文档 |
|---|------|----------|------|
| 1 | 地图管理 | upload / openmap / metadata / listmaps / thumbnail | `api/01-map-management.md` |
| 2 | 瓦片/样式 | tile / mvt / wms / createMapStyle / switchlayer | `api/02-tiles-and-styles.md` |
| 3 | 要素查询 | queryFeatures / createEntityGeoData | `api/03-query.md` |
| 4 | 图形分析 | mapDiff / mapCompare / extractTable / centerLines | `api/04-analysis.md` |
| 5 | 组合/导出 | composeNewMap / exportPdf / exportDxf | `api/05-compose-and-export.md` |
| 6 | WebCAD | listWebcadDraws / saveWebcadPatch（可选） | `api/06-webcad.md` |
| 7 | 工作区 | getWorkspace / workspaceCreate | `api/07-workspace.md` |
| 8 | KV 存储 | data/{key} / datas / datakeys | `api/08-data-storage.md` |
| 9 | 全文搜索 | fullsearch/search / add / delete | `api/09-fullsearch.md` |
| 10 | 符号库 | symbol/categories / symbol/list | `api/10-symbols.md` |
| 11 | 系统 | heart / version / runstatus / viewlogs | `api/11-system.md` |
| 12 | /service 杂项 | qrcode / fonts / uploadImg | `api/12-service-misc.md` |
| 13 | AI（可选） | vectorSearch / chatFaq / vectorDocs | `api/13-ai.md` |

## 4. 该怎么读这份文档？

按兴趣从下面挑一条路径：

1. **第一次接触 vjmap**：先过 [`01-getting-started.md`](01-getting-started.md) 把 Python 或 Node SDK 跑起来。
2. **看得懂 curl 就够**：直接翻 `api/01-map-management.md`，里面每个接口都有 `curl` + Python + Node 三列。

## 5. 命名与术语

| 术语 | 含义 |
|------|------|
| `mapid` | 图唯一 ID；通常是业务名或 `_` 占位 |
| `version` | 图版本号；默认 `v1` |
| `layer` | 图层名；多数 cmd 通过请求体 `layers` 字段指定 |
| `fileid` | 源文件 ID，一般等于 md5 前缀 |
| `secretKey` / `accessKey` | 加密图的访问密钥（前者 16 字符，后者 `ak` 开头 18 字符） |
| `workspace` | 命名工作区；默认工作区等价于 `""`（空） |
| `geom render` | 图形服务子进程，负责几何渲染/切片 |

## 6. 约定（Conventions）速览

- 所有 `/api/v1/map/...` 接口需要 `token=<JWT>`（query 或 header）
- 95 % 的 cmd 接口既接受 GET 也接受 POST；SDK 统一走 POST
- 成功响应结构两类：
  - 网关包装 `{code:0, msg:"", data:{...}}`
  - 图形服务 JSON（直接是 `{...}` 或数组）；SDK 自动识别并 unwrap
- 错误：HTTP 非 2xx → 抛 `VjmapError`；网关 `{code≠0}` 也视为错误抛出
- 详细规则见 [`02-conventions.md`](02-conventions.md)

---

## 01-getting-started.md
<a id=01-getting-startedmd></a>

# 快速上手（Getting Started）

在 5 分钟内用 Python 或 Node.js SDK 跑通 vjmap 的最小闭环。

## 1. 前置条件

- 已部署的 vjmap 服务，默认运行在 `http://127.0.0.1:27660`
- 一个有效 JWT Token（开发环境可直接从后端控制台获取；生产请走登录接口）
- Python ≥ 3.8 **或** Node.js ≥ 18

## 2. 健康检查

服务是否活着：

```bash
curl -s http://127.0.0.1:27660/heart
# => {"status":"ok","timestamp":"..."}

curl -s http://127.0.0.1:27660/version
# => {"arch":"amd64","os":"windows","status":"ok","time":"...","version":"..."}
```

## 3. Python 一分钟体验

```bash
# 1. 拷贝 SDK
cp sdk/python/vjmap.py your-project/

# 2. 装依赖
pip install requests

# 3. hello world
python - <<'PY'
from vjmap import Vjmap
svc = Vjmap("http://127.0.0.1:27660/api/v1", "<paste-token>")
print("heart   ->", svc.heart())
print("version ->", svc.version())
print("status  ->", svc.run_status(detail=True)["application"])
PY
```

期望输出：

```text
heart   -> {'status': 'ok', 'timestamp': '...'}
version -> {'arch': 'amd64', 'os': 'windows', 'status': 'ok', 'time': '...', 'version': '2026041001'}
status  -> {'pid': '...', 'runtime': ..., ...}
```

## 4. Node.js 一分钟体验

```bash
cp sdk/nodejs/vjmap.js your-project/
node - <<'JS'
const { Vjmap } = require('./vjmap');
(async () => {
  const svc = new Vjmap('http://127.0.0.1:27660/api/v1', '<paste-token>');
  console.log('heart   ->', await svc.heart());
  console.log('version ->', await svc.version());
  console.log('status  ->', (await svc.runStatus()).application);
})();
JS
```

## 5. 最常用的 4 步闭环

> 下面示例用 Python；Node 版本见 `examples/nodejs/`。

### 5.1 列出已有的图

```python
maps = svc.list_maps()           # 对应 /api/v1/map/cmd/listmaps/_/v1
for mapid, versions in maps.items():
    print(mapid, [v["version"] for v in versions])
```

### 5.2 打开图并读元数据

```python
svc.open_map(mapid="sys_zp", version="v1", mapopenway="GeomRender")
meta = svc.metadata()
print("layers:", [l["name"] for l in meta.get("layers", [])])
print("bounds:", meta.get("bounds"))
```

### 5.3 取栅格瓦片 URL（ XYZ source）

```python
url = svc.raster_tile_url(mapid="sys_zp", version="v1",
                          layer="0,1,2,3,4,5,6,7,8", z="{z}", x="{x}", y="{y}")
print(url)
# => http://127.0.0.1:27660/api/v1/map/tile/sys_zp/v1/0,1,.../{z}/{x}/{y}?token=...
```

把 URL 直接填到 vjmap 的 `raster` source 就能显示。

### 5.4 关闭图（可选，释放 图形服务 子进程）

```python
svc.close_map(mapid="sys_zp", version="v1")
```

## 6. 下一步

- 想系统了解参数规则？→ [`02-conventions.md`](02-conventions.md)
- 弄清 token / secretKey？→ [`03-authentication.md`](03-authentication.md)
- 多租户分区？→ [`04-workspace.md`](04-workspace.md)
- 跳到接口分类索引 → [`api/index.md`](api/index.md)

---

## 02-conventions.md
<a id=02-conventionsmd></a>

# 通用约定（Conventions）

所有接口的共性规则集中在这里，具体接口文档不再重复。

## 1. Base URL 与前缀

- 默认：`http://127.0.0.1:27660/api/v1`
- 文档里写成 `{base}`；SDK 自动补 `/` 结尾
- 三种子前缀：

| 前缀 | 用途 |
|------|------|
| `{base}/map/...` | 默认工作区的所有图相关接口 |
| `{base}/workspace/{name}/...` | 命名工作区下的图相关接口（与 `/map/` 等价但隔离存储） |
| `{base}/service/...` | 与图无关的工作区/KV/符号库/全文搜索/AI 等接口 |

系统级根路径：

| 路径 | 说明 |
|------|------|
| `/heart` | 健康检查 |
| `/version` | 版本 |

## 2. HTTP 方法

- **cmd 接口**（路径形如 `/api/v1/map/cmd/<cmd>/<mapid>/<version>`）**既接受 GET 也接受 POST**；网关 `Router.Any` 统一挂载。SDK 默认走 **POST**，原因：
  1. 请求体大时 GET 超长
  2. 语义上是"动作"而非"只读资源"
- 其它接口遵循标准 REST：GET 查、POST 创建、PUT 修改、DELETE 删除

## 3. 鉴权

> 详情见 [`03-authentication.md`](03-authentication.md)。

- **必带**：`token=<JWT>`（推荐放 query，也可放 header）
- **加密图**：请求头 `secretKey: <key1>_;_<key2>...` 或 query `secret_key=...`
- **全局 access key**（`ak` 开头 18 位）：可放在 `accessKey` 位置用来**一次性访问多张加密图**

## 4. 路径占位符

| 占位 | 常用取值 | 含义 |
|------|---------|------|
| `{mapid}` | `sys_zp`、`_`、`_null` | 图 ID；`_` / `_null` 用在不需要 mapid 的 cmd 上 |
| `{version}` | `v1`、`_` | 版本号；`_` 表示与具体版本无关 |
| `{layer}` | `"0,1,2"`、`default`、空 | 图层名/ID；多个用逗号 |
| `{z}/{x}/{y}` | `10/854/402` | 标准 XYZ 瓦片坐标 |
| `{key}` | 用户自定义 | KV 存储的 key（建议 URL-safe） |

**重要**：SDK 会对占位符做 `encodeURIComponent`，别再手动编码。

## 5. 请求体约定

| 场景 | Content-Type | 备注 |
|------|--------------|------|
| 绝大多数 cmd | `application/json` | SDK 默认 |
| 文件上传（`/map/uploads`、`/service/uploadImg`） | `multipart/form-data` | 字段名 `file` |
| KV 存值 `POST /data/{key}` | `application/x-www-form-urlencoded` | 字段 `data=` |

## 6. 响应结构

两类并存，SDK 都能识别：

### 6.1 web服务 网关标准响应

```json
{
  "code": 0,
  "msg": "",
  "data": { /* 实际内容 */ }
}
```

- `code == 0` → SDK unwrap 成 `data`
- `code != 0` → SDK 抛 `VjmapError(status=200, body={...})`

### 6.2 图形服务

```json
{
  "bounds": [...],
  "layers": [...],
  ...
}
```

直接返回，不包 `{code,msg,data}`。

### 6.3 错误响应

- HTTP 4xx / 5xx：SDK 抛 `VjmapError(status=4xx|5xx, url, body)`
- cmd 执行失败但网关路由成功：多数 cmd 用 `{error: "..."}` 或 `{result: {succeed: false}}` 表达；SDK 不会自动判断业务成功/失败，请业务侧自行校验

## 7. 分页 / 列表

大部分列出接口没有分页，一次返回全部：

- `listmaps`：按 mapid 聚合，返回 `{mapid: [ {version, db, filename, ...} ]}`
- `symbol/list/{cat}`：数组
- `getWorkspace`：数组

少数有 `limit` / `offset` / `pageSize` / `pageNum`（例如 `fullsearch/search`），每个接口文档单独说明。

## 8. 幂等性与重试

| 操作 | 幂等 | 可自动重试？ |
|------|------|-------------|
| `openmap` (GeomRender) | 否 | 否，会占用 图形服务 子进程 |
| `metadata` / `listmaps` / `runstatus` | 是 | 是 |
| `updatemap` / `updateMetadata` | 否 | 否 |
| `deletemap` / `deletestyle` | 是 | 是，但业务需防误触 |
| 瓦片 GET | 是 | 是 |

SDK 不内置重试；

## 9. 超时

- SDK 默认 60 秒；超长 cmd（`composeNewMap`、`saveOfflineTileDb`、`extractTable` on 大图）可能要 3-5 分钟，调用时显式 `timeout=300`（秒）。
- 图形服务单请求默认 `600000 ms = 10 min`（`runstatus.process.reqTimeout`），超过会被强杀。

## 10. 文件下载 / 流式接口

导出 PDF、DXF、离线瓦片包、缩略图等接口返回 **二进制流**。SDK 里对应方法：

- Python：`svc._request("POST", url, stream=True)` 返回原始 `requests.Response`，自己 `.iter_content()` 写文件
- Node：`svc._request('POST', url, { stream: true })` 返回原始 `Response`，自己 `await res.arrayBuffer()` 或管道到文件

## 11. 命名风格（跨语言）

| 语言 | 风格 | 例 |
|------|------|----|
| Python SDK | `snake_case` | `open_map`, `close_map`, `raster_tile_url` |
| Node SDK | `camelCase` | `openMap`, `closeMap`, `rasterTileUrl` |
| 文档正文 | 二者并列列出 | 每个接口都给两种示例 |

## 12. 日志与可观测

- `runstatus?detail=true`：看引擎 pid、活跃请求、子进程等
- `viewlogs?record=100`：看最近 100 条日志；含 cmd 耗时、错误

---

## 03-authentication.md
<a id=03-authenticationmd></a>

# 鉴权与加密图（Authentication）

## 1. JWT Token

- vjmap 后端用 **标准 HS256 JWT** 做鉴权
- Claims 里包含 `ID` / `Username` / `AuthorityId` / `BufferTime` / `exp` / `iss=vjmap`
- 过期时间：默认 `BufferTime=86400 秒`；`exp` 远期刷新由 `{UpdateTokenRoute}` 负责
- 生产环境拿 token 的方式：通过业务登录接口；开发环境可直接从 vjmap 后台复制

### 1.1 Token 放哪里

三种都接受，**优先级从高到低**：

1. HTTP Header：`token: eyJ...`
2. Query：`?token=eyJ...`
3. Form：`token=eyJ...`（仅个别接口）

SDK 默认走 **Query**（便于 curl 排障）；如果你的反代会剥 query，可传 `header=True`（Python）/`{headers:{token}}` 客户端自行覆盖。

### 1.2 Token 的 5 分钟内测

```bash
TOKEN="eyJhbG..."
curl -s "http://127.0.0.1:27660/version?token=$TOKEN"
curl -s "http://127.0.0.1:27660/api/v1/map/cmd/runstatus/_/v1?token=$TOKEN&detail=true"
```

### 1.3 常见错误

| 现象 | 原因 |
|------|------|
| `HTTP 401` | token 过期 / 签名不匹配 |
| `{code: 10006, msg: "..."}`（网关响应） | token 合法但权限不够（例如不是 root）|
| `HTTP 200` 但业务字段异常 | token OK，但参数错误 |

## 2. 加密图（secretKey / accessKey）

vjmap 允许对图设置访问密钥，打开/瓦片/元数据请求都要带。

### 2.1 secretKey（每图一把，16 字符）

- 创建/上传图时可通过 `secretKey` 字段设置
- 明文密码 → `secretKey` 算法（与 Service.ts `pwdToSecretKey` 一致）：
  - 连续 4 次 md5 后取前 16 字符
  - 空串 → 空串
  - 长度 == 18 且以 `ak` 开头 → 视为 accessKey，原样返回
- SDK：`compute_secret_key(pwd)` / `computeSecretKey(pwd)`

### 2.2 accessKey（全局，18 字符 `ak...`）

- 跨图通用的"超级密钥"，典型用途：服务端代为访问所有图
- 放在请求头/query 里的字段也叫 `secretKey`，但值是 18 位 ak 串
- SDK 识别：如果密钥串长度 == 18 且前缀 `ak`，不再 md5 拼装，直接透传

### 2.3 多密钥传递

一个请求可能同时携带多个 secretKey（覆盖多张图）：

- Header: `secretKey: key1_;_key2_;_ak1234...abcd`
- Query: 用多个 `secret_key=` 或一个 `secret_key=key1_;_key2`

SDK Python：

```python
svc._secret_keys = ["<16chars>", "<16chars>", "ak........"]
# 后续请求会自动拼 header
```

SDK Node：

```javascript
svc._secretKeys = [ '16chars...', 'ak........' ];
```

### 2.4 加密图典型流程

```python
svc = Vjmap(base, token)
svc._secret_keys = [compute_secret_key("myPass123")]
svc.open_map(mapid="finance_map", version="v1", mapopenway="GeomRender",
             secretKey="myPass123")   # 打开时也可带明文
meta = svc.metadata()                  # 会自动 carry secretKey
```

## 3. Super Key（运维视角）

`.env` 里的 `VJMAP_MAP_SUPER_KEY` 本质是一把 **accessKey**，含义是：

- 整个实例的所有加密图都接受它
- 二次开发一般不需要它；**只有**测试框架需要跑加密场景时才用

## 4. CSRF / 同源

- 后端不强制 CSRF token；依赖 JWT 签名
- 浏览器侧调用请注意 CORS：网关默认允许 `Origin: *`；如果在受限部署里碰到 CORS，需要运维修改 `ReqMiddlewareCors`

## 5. 速率限制

- **默认无全局速率限制**
- 单个 cmd 的并发由 `runstatus.maxParallelProcessGeomNum`（几何渲染）控制，典型值 2-4
- 超并发请求会被排队，不会报错但会"看起来卡住"

## 6. 过期 Token 的自动刷新

网关里有一个 `{UpdateTokenRoute}`（常见为 `/api/v1/map/updateToken`）。

---

## 04-workspace.md
<a id=04-workspacemd></a>

# 工作区（Workspace）

工作区是 vjmap 的**命名空间机制**：不同工作区下的图、符号、KV 存储、全文索引**互相隔离**。

## 1. 概念

- **默认工作区**：不带名字；URL 用 `/api/v1/map/...` 前缀
- **命名工作区**：人为创建；URL 变为 `/api/v1/workspace/{name}/...` 前缀
- 工作区本身也有状态：`{name, description, createTime, ...}`，由 `/api/v1/service/getWorkspace` 列出
- 一个 token 理论上可访问所有工作区；实际权限由 `AuthorityId` 决定

## 2. 切换工作区（SDK）

### Python

```python
svc = Vjmap("http://127.0.0.1:27660/api/v1", token)
svc.switch_workspace("team_a")          # 之后所有 map_url / cmd_url 都会自动带上 /workspace/team_a
svc.open_map("finance", "v1", mapopenway="Memory")  # → /api/v1/workspace/team_a/cmd/openmap/finance/v1
svc.switch_workspace("")                # 切回默认工作区
```

### Node

```javascript
svc.switchWorkspace('team_a');
await svc.openMap({ mapid: 'finance', version: 'v1', mapopenway: 'Memory' });
svc.switchWorkspace('');
```

## 3. 生命周期接口

| # | 操作 | URL | 方法 |
|---|------|------|------|
| 1 | 列出 | `/api/v1/service/getWorkspace` | GET |
| 2 | 创建 | `/api/v1/service/workspaceCreate` | POST |
| 3 | 修改 | `/api/v1/service/workspaceModify` | POST |
| 4 | 删除 | `/api/v1/service/workspaceDelete/{name}` | DELETE |
| 5 | 状态 | `/api/v1/service/workspaceStatus` | GET |

详细字段、响应示例请看 [`api/07-workspace.md`](api/07-workspace.md) 。

## 4. 底层 URL 改写规则

- web服务网关把 `/api/v1/workspace/{name}/*` 改写成 **带 workspace 参数**的 `/api/v1/map/*`；图形服务不感知名字，由网关保证隔离
- 因此凡是 `/api/v1/map/` 下的路径都能在 `/workspace/{name}/` 下一模一样使用
- `/api/v1/service/*`（全局 KV、符号库、全文搜索）**不受**工作区前缀影响；它们的隔离通过各自的 `workspace` 参数实现（大部分接口接受 `workspace=` query）

## 5 反模式

- 不要在**热路径**里频繁调用 `switch_workspace()` 再立刻发请求；SDK 内部只是改前缀，没 IO，但业务代码可能会混乱。建议**一个 svc 实例绑一个 workspace**。
- 不要把 `workspace` 和 `mapid` 拼到一起当复合键用；保持**每图一个 mapid，workspace 单独字段**。

---

## api/index.md
<a id=apiindexmd></a>

# 接口文档总索引（API Index）

> 本页把 13 个分类、73 个 接口分别映射到具体文档、SDK 方法。

## 0. 阅读顺序建议

1. 先读 [`../00-overview.md`](../00-overview.md)、[`../02-conventions.md`](../02-conventions.md)
2. 再按需要翻下面分类文档
3. 每个接口文档包含 4 块：**URL & 方法** / **入参** / **出参** / **示例（curl + Python + Node + 真实响应）**

## 1. 分类一览

| # | 分类 | 接口数 | 文档 | Phase |
|---|------|-------:|------|-------|
| 01 | 地图管理（Map Management） | 15 | [`01-map-management.md`](01-map-management.md) | C.1 |
| 02 | 瓦片 & 样式（Tiles & Styles） | 10 | [`02-tiles-and-styles.md`](02-tiles-and-styles.md) | C.2 |
| 03 | 要素查询（Query） | 4 | [`03-query.md`](03-query.md) | C.3 |
| 04 | 图形分析（Analysis） | 6 | [`04-analysis.md`](04-analysis.md) | C.4 |
| 05 | 组合 / 导出（Compose & Export） | 6 | [`05-compose-and-export.md`](05-compose-and-export.md) | C.5 |
| 06 | WebCAD（可选） | 6 | [`06-webcad.md`](06-webcad.md) | C.6 |
| 07 | 工作区（Workspace） | 5 | [`07-workspace.md`](07-workspace.md) | C.7 |
| 08 | KV 存储（Data Storage） | 5 | [`08-data-storage.md`](08-data-storage.md) | C.8 |
| 09 | 全文搜索（Full-text Search） | 3 | [`09-fullsearch.md`](09-fullsearch.md) | C.9 |
| 10 | 符号库（Symbols） | 3 | [`10-symbols.md`](10-symbols.md) | C.10 |
| 11 | 系统（System） | 4 | [`11-system.md`](11-system.md) | C.11 |
| 12 | /service 杂项（Service Misc） | 3 | [`12-service-misc.md`](12-service-misc.md) | C.12 |
| 13 | AI（可选） | 3 | [`13-ai.md`](13-ai.md) | C.13 |
| - | **合计** | **73** | - | - |

## 2. 快速找接口（常用任务 → 文档）

| 我要做…… | 看这里 |
|----------|--------|
| 上传一个 DWG 文件并在后端渲染 | `01-map-management.md` § uploads / openmap |
| 读元数据（图层列表、范围） | `01-map-management.md` § metadata |
| 生成 XYZ 瓦片 URL | `02-tiles-and-styles.md` § tile / mvt |
| 切换显隐图层并应用样式 | `02-tiles-and-styles.md` § createMapStyle / switchlayer |
| 找几何要素 | `03-query.md` § queryFeatures |
| 两图对比 | `04-analysis.md` § mapDiff / mapCompare |
| 导出 PDF / DXF | `05-compose-and-export.md` |
| WebCAD 修改落盘 | `06-webcad.md` |
| 存一段用户配置 JSON | `08-data-storage.md` § data/{key} |
| 全文搜索 | `09-fullsearch.md` |
| 看服务是否在运行 | `11-system.md` § heart / runstatus |

## 3. SDK 方法名速查

Python 与 Node 的方法名一一对应（snake_case ↔ camelCase）：

| Python | Node | 对应 URL |
|--------|------|---------|
| `heart()` | `heart()` | `GET /heart` |
| `version()` | `version()` | `GET /version` |
| `run_status(detail)` | `runStatus(detail)` | `GET /api/v1/map/cmd/runstatus/_/v1` |
| `upload_map(...)` | `uploadMap(...)` | `POST /api/v1/map/uploads` |
| `check_map_file(md5)` | `checkMapFile(md5)` | `GET /api/v1/map/mapfile` |
| `open_map(mapid, version, ...)` | `openMap({mapid, version, ...})` | `GET/POST /api/v1/map/openmap/{mapid}` |
| `update_map(...)` | `updateMap(...)` | `POST /api/v1/map/updatemap/{mapid}` |
| `close_map(mapid, version)` | `closeMap(mapid, version)` | `POST /api/v1/map/cmd/closemap/{mapid}/{version}` |
| `delete_map(mapid, version)` | `deleteMap(mapid, version)` | `POST /api/v1/map/cmd/deletemap/{mapid}/{version}` |
| `rename_map(old, new)` | `renameMap(old, new)` | `POST /api/v1/map/cmd/renamemap/{old}/_` |
| `list_maps()` | `listMaps()` | `POST /api/v1/map/cmd/listmaps/_/v1` |
| `metadata(mapid, version)` | `metadata(mapid, version)` | `POST /api/v1/map/cmd/metadata/{mapid}/{version}` |
| `update_metadata(...)` | `updateMetadata(...)` | `POST /api/v1/map/cmd/updateMetadata/{mapid}/{version}` |
| `thumbnail_url(...)` | `thumbnailUrl(...)` | `GET /api/v1/map/cmd/thumbnail/{mapid}/{version}` |
| `support_format()` | `supportFormat()` | `GET /api/v1/map/cmd/supportFormat/_/v1` |
| `const_data()` | `constData()` | `GET /api/v1/map/cmd/constData/_/v1` |
| `get_data_bounds(mapid, version)` | `getDataBounds(mapid, version)` | `POST /api/v1/map/cmd/getDataBounds/{mapid}/{version}` |
| `raster_tile_url(...)` | `rasterTileUrl(...)` | `GET /api/v1/map/tile/...` |
| `vector_tile_url(...)` | `vectorTileUrl(...)` | `GET /api/v1/map/tile/.../{z}/{x}/{y}.mvt` |
| `wms_tile_url(...)` | `wmsTileUrl(...)` | `GET /api/v1/map/cmd/wms/...` |
| `create_map_style(...)` | `createMapStyle(...)` | `POST /api/v1/map/cmd/createMapStyle/...` |
| `cmd_switch_layers(...)` | `cmdSwitchLayers(...)` | `POST /api/v1/map/cmd/switchlayer/...` |
| `cmd_update_style(...)` | `cmdUpdateStyle(...)` | `POST /api/v1/map/cmd/updateStyle/...` |
| `slice_layer(...)` | `sliceLayer(...)` | `POST /api/v1/map/cmd/slicelayer/...` |
| `get_slice_cache_zoom(...)` | `getSliceCacheZoom(...)` | `GET /api/v1/map/cmd/getSliceCacheZoom/...` |
| `delete_style(...)` | `deleteStyle(...)` | `POST /api/v1/map/cmd/deletestyle/...` |
| `query_features(...)` | `queryFeatures(...)` | `POST /api/v1/map/cmd/queryFeatures/...` |
| `create_entity_geo_data(...)` | `createEntityGeoData(...)` | `POST /api/v1/map/cmd/createEntityGeoData/_null/v1` |
| `coord_transform(...)` | `coordTransform(...)` | `POST /api/v1/map/cmd/coordtransform/_/_` |
| `prj_wkt_to_prj4_str(...)` | `prjWktToPrj4Str(...)` | `POST /api/v1/map/cmd/prjWktToPrj4Str/_/_` |
| `cmd_map_diff(...)` | `cmdMapDiff(...)` | `POST /api/v1/map/cmd/mapDiff/_null/v1` |
| `cmd_map_compare(...)` | `cmdMapCompare(...)` | `POST /api/v1/map/cmd/mapCompare/_null/v1` |
| `cmd_extract_table(...)` | `cmdExtractTable(...)` | `POST /api/v1/map/cmd/extractTable/_null/v1` |
| `cmd_extract_center_lines(...)` | `cmdExtractCenterLines(...)` | `POST /api/v1/map/cmd/extractCenterLines/_null/v1` |
| `cmd_match_object(...)` | `cmdMatchObject(...)` | `POST /api/v1/map/cmd/objectMatch/_null/v1` |
| `cmd_image_svg_to_dwg(...)` | `cmdImageSvgToDwg(...)` | `POST /api/v1/map/cmd/imageSvgToDwg/_null/v1` |
| `cmd_compose_new_map(...)` | `cmdComposeNewMap(...)` | `POST /api/v1/map/cmd/composeNewMap/_null/v1` |
| `cmd_split_map(...)` | `cmdSplitMap(...)` | `POST /api/v1/map/cmd/cmdSplitMap/_null/v1` |
| `cmd_export_layout(...)` | `cmdExportLayout(...)` | `POST /api/v1/map/cmd/exportLayout/_null/v1` |
| `cmd_export_pdf(...)` | `cmdExportPdf(...)` | `POST /api/v1/map/cmd/exportPdf/{mapid}/{version}` |
| `cmd_export_dxf(...)` | `cmdExportDxf(...)` | `POST /api/v1/map/cmd/exportDxf/{mapid}/{version}` |
| `cmd_save_offline_tile_db(...)` | `cmdSaveOfflineTileDb(...)` | `POST /api/v1/map/cmd/saveOfflineTileDb/_null/v1` |
| `list_webcad_draws()` | `listWebcadDraws()` | `GET /api/v1/map/cmd/listWebcadDraws/_/v1` |
| `get_webcad_data(...)` | `getWebcadData(...)` | `POST /api/v1/map/cmd/getWebcadData/...` |
| `save_webcad_patch(...)` | `saveWebcadPatch(...)` | `POST /api/v1/map/cmd/saveWebcadPatch/...` |
| `delete_webcad_draw(...)` | `deleteWebcadDraw(...)` | `POST /api/v1/map/cmd/deleteWebcadDraw/...` |
| `convert_webcad(...)` | `convertWebcad(...)` | `POST /api/v1/map/cmd/convertWebCAD/...` |
| `export_webcad(...)` | `exportWebcad(...)` | `POST /api/v1/map/cmd/exportWebCAD/_null/v1` |
| `get_workspaces()` | `getWorkspaces()` | `GET /api/v1/service/getWorkspace` |
| `workspace_create(...)` | `workspaceCreate(...)` | `POST /api/v1/service/workspaceCreate` |
| `workspace_modify(...)` | `workspaceModify(...)` | `POST /api/v1/service/workspaceModify` |
| `workspace_delete(name)` | `workspaceDelete(name)` | `DELETE /api/v1/service/workspaceDelete/{name}` |
| `workspace_status()` | `workspaceStatus()` | `GET /api/v1/service/workspaceStatus` |
| `get_custom_data(key)` | `getCustomData(key)` | `GET /api/v1/service/data/{key}` |
| `save_custom_data(key, val)` | `saveCustomData(key, val)` | `POST /api/v1/service/data/{key}` |
| `delete_custom_data(key)` | `deleteCustomData(key)` | `DELETE /api/v1/service/data/{key}` |
| `save_multiple_custom_data(...)` | `saveMultipleCustomData(...)` | `POST /api/v1/service/datas` |
| `get_custom_data_keys(prefix)` | `getCustomDataKeys(prefix)` | `GET /api/v1/service/datakeys` |
| `full_search(...)` | `fullSearch(...)` | `GET/POST /api/v1/service/fullsearch/search` |
| `full_search_add(...)` | `fullSearchAdd(...)` | `POST /api/v1/service/fullsearch/add` |
| `full_search_delete(...)` | `fullSearchDelete(...)` | `DELETE /api/v1/service/fullsearch/delete` |
| `symbol_categories()` | `symbolCategories()` | `GET /api/v1/service/symbol/categories` |
| `symbol_list(cat)` | `symbolList(cat)` | `GET /api/v1/service/symbol/list/{cat}` |
| `symbol_get(id)` | `symbolGet(id)` | `GET /api/v1/service/symbol/{id}` |
| `view_logs(record)` | `viewLogs(record)` | `GET /api/v1/map/cmd/viewlogs/_/v1` |
| `qrcode_url(content, size)` | `qrcodeUrl(content, size)` | `GET /api/v1/service/qrcode` |
| `glyphs_url(fontstack, range)` | `glyphsUrl(fontstack, range)` | `GET /api/v1/service/fonts/...` |
| `upload_img(file)` | `uploadImg(file)` | `POST /api/v1/service/uploadImg` |
| `vector_search(...)` | `vectorSearch(...)` | `POST /api/v1/service/vectorSearch` |
| `chat_faq(q)` | `chatFaq(q)` | `GET /api/v1/service/chatFaq` |
| `vector_docs(...)` | `vectorDocs(...)` | `GET /api/v1/service/vectorDocs` |

>

---

## api/01-map-management.md
<a id=api01-map-managementmd></a>

# 地图管理（Map Management）

> 分类 1 / 13 · 接口 15 个 · 

**快速目录**

| # | 接口 | 方法 | URL |
|---|------|------|-----|
| 1 | [秒传检测](#1-秒传检测-check_map_file) | GET | `/api/v1/map/mapfile` |
| 2 | [上传源文件](#2-上传源文件-upload_map) | POST | `/api/v1/map/uploads` |
| 3 | [打开/创建地图](#3-打开创建地图-open_map) | POST | `/api/v1/map/openmap/{mapid}` |
| 4 | [修改地图](#4-修改地图-update_map) | POST | `/api/v1/map/updatemap/{mapid}` |
| 5 | [关闭地图](#5-关闭地图-close_map) | GET | `/api/v1/map/cmd/closemap/{mapid}/{version}` |
| 6 | [删除地图](#6-删除地图-delete_map) | GET | `/api/v1/map/cmd/deletemap/{mapid}/{version}` |
| 7 | [重命名地图](#7-重命名地图-rename_map-已知不稳定) | GET | `/api/v1/map/cmd/renamemap/{old}/_` |
| 8 | [删除源文件](#8-删除源文件-delete_map_file) | GET | `/api/v1/map/cmd/deletemapfile/_/v1` |
| 9 | [列出地图](#9-列出地图-list_maps) | GET | `/api/v1/map/cmd/listmaps/_/v1` |
| 10 | [读元数据](#10-读元数据-metadata) | GET | `/api/v1/map/cmd/metadata/{mapid}/{version}` |
| 11 | [修改元数据](#11-修改元数据-update_metadata) | POST | `/api/v1/map/cmd/updateMetadata/{mapid}/{version}` |
| 12 | [缩略图](#12-缩略图-thumbnail) | GET | `/api/v1/map/cmd/thumbnail/{mapid}/{version}` |
| 13 | [支持的格式](#13-支持的格式-support_format) | GET | `/api/v1/map/cmd/supportFormat/_/v1` |
| 14 | [系统常量](#14-系统常量-const_data) | GET | `/api/v1/map/cmd/constData/_/v1` |
| 15 | [数据范围](#15-数据范围-get_data_bounds) | GET | `/api/v1/map/cmd/getDataBounds/{mapid}/{version}` |

**共性约定（重要）**

- 所有 `cmd/*` 路径：`GET` **不需要 body**，`POST` **要求 body 至少一个字段**，否则后端回 500 `param error`。SDK 对无 body 的 cmd 统一走 GET。
- `openmap` 通过 **POST body** 传 `mapid/version/fileid/uploadname/mapopenway`；`fileid ` 必须来自 `upload_map` 的返回，不要自己拼 md5。
- 所有路径都带 `?token=<JWT>`（SDK 自动加）。

---

## 1. 秒传检测 `check_map_file`

**URL**：`GET /api/v1/map/mapfile?md5={md5}`  
**用途**：客户端上传前先算文件 md5，若 `result:true` 则复用已有文件，跳过上传。

### 入参（Query）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `md5` | string(32) | 是 | 源文件 MD5（小写 hex） |
| `token` | string | 是 | JWT |

### 出参

```json
{"result": false}
```

- `result=true`：已有同 md5 文件；可直接跳过上传用 `fileid = md5[:12]`
- `result=false`：需要走 `upload_map`

### 示例

```bash
curl -s "http://127.0.0.1:27660/api/v1/map/mapfile?token=$T&md5=caf27de8a2259b2c4e04f2afde2f5c42"
```

```python
svc.check_map_file("caf27de8a2259b2c4e04f2afde2f5c42")
```

```javascript
await svc.checkMapFile('caf27de8a2259b2c4e04f2afde2f5c42');
```

---

## 2. 上传源文件 `upload_map`

**URL**：`POST /api/v1/map/uploads`  
**Content-Type**：`multipart/form-data`  

### 入参（Form）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `file` | binary | 是 | 源文件 |
| `md5` | string(32) | 是 | 预算的 MD5 |
| `secretKey` | string | 否 | 设置加密图的 16 位密钥；SDK 中默认空 |

### 出参

```json
{"fileid":"caf27de8a225","mapid":"caf27de8a225","uploadname":"caf27de8a225.dwg"}
```

- `fileid`：生成的**12 位十六进制**（= md5 前 12 位）
- `mapid`：默认等于 `fileid`；后续 `open_map` 可以给另一个业务名
- `uploadname`：服务端实际落盘的文件名

### 示例

```bash
curl -s -X POST "http://127.0.0.1:27660/api/v1/map/uploads?token=$T" \
  -F "file=@./my.dwg" \
  -F "md5=$(md5sum my.dwg | awk '{print $1}')"
```

```python
up = svc.upload_map("./my.dwg")
fileid, uploadname = up["fileid"], up["uploadname"]
```

```javascript
const up = await svc.uploadMap('./my.dwg');
const { fileid, uploadname } = up;
```

---

## 3. 打开/创建地图 `open_map`

**URL**：`POST /api/v1/map/openmap/{mapid}?version={version}` （SDK 默认走 POST；GET 等价）  

这是整套 API 的**中心接口**。它同时承担：

- 首次创建：`mapid` 未存在时结合 `fileid + uploadname` 新建
- 再次打开：`mapid` 已存在时忽略 `fileid/uploadname`
- 切换模式：`mapopenway` 决定是否拉起  渲染子进程

### 入参（JSON body）

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `mapid` | string | 是 | 业务 ID |
| `version` | string | 是 | 版本号，默认 `v1` |
| `fileid` | string(12) | **首次建图必填** | `upload_map` 返回的 fileid |
| `uploadname` | string | **首次建图必填** | `upload_map` 返回的 uploadname |
| `mapopenway` | `Memory` / `GeomRender` | 是 | 见 [`../02-conventions.md`](../02-conventions.md) |
| `style` | object | 否 | 预设样式；详情见 § 2 `create_map_style` |
| `secretKey` | string | 否 | 图级密钥 |
| `accessKey` | string | 否 | 全局 ak |
| `mapbounds` | array / string | 否 | 限制的图面范围 |

### 出参（节选，完整见 snapshot）

| 字段 | 含义 |
|------|------|
| `mapid / version / fileid / uploadname / filename` | 基本定位 |
| `bounds / dbBounds / drawBounds / initBounds / viewBounds` | 各种范围 |
| `layers` *(JSON string)* | 图层数组 |
| `styles` *(JSON string)* | 预置样式数组 |
| `format` | 瓦片格式（典型 `png`） |
| `mapopenway` | 实际使用的打开方式 |
| `pid` |  子进程 PID（GeomRender 时） |
| `renderAccuracy` | 渲染精度 |
| `isnewmap` | 是否刚创建 |

#### 真实响应片段

```json
{
  "mapid": "nb_roundtrip_1776829368",
  "version": "v1",
  "fileid": "caf27de8a225",
  "uploadname": "caf27de8a225.dwg",
  "filename": "caf27de8a225.dwg",
  "mapopenway": "Memory",
  "bounds": "[67108.08576259,-10253.62403246,73355.58576721,-4006.12402784]",
  "isnewmap": true,
  "format": "png",
  "layers": "[{\"color\":\"#ffffff\",\"index\":0,...}]",
  "styles": "[{\"backcolor\":0,\"isgeom\":false,...}]"
}
```

### 示例

```python
svc.open_map(
    mapid="demo_dwg", version="v1",
    mapopenway="Memory",
    fileid=up["fileid"], uploadname=up["uploadname"],
)
```

```javascript
await svc.openMap({
  mapid: 'demo_dwg', version: 'v1',
  mapopenway: 'Memory',
  fileid: up.fileid, uploadname: up.uploadname,
});
```

---

## 4. 修改地图 `update_map`

**URL**：`POST /api/v1/map/updatemap/{mapid}?version={version}`  

用法：修改 **已存在**图的属性（描述、可见性、部分字段等）。实现上等价于"带新参数再打开一次"，因此 body 通常和 `open_map` 类似，加上要改的字段。

### 入参（JSON body）

| 字段 | 必填 | 说明 |
|------|------|------|
| `mapid` | 是 | 要更新的图 |
| `fileid` | 是 | 必须和打开时一致 |
| `uploadname` | 是 | 同上 |
| `mapopenway` | 是 | 同上 |
| `description` | 否 | 描述 |
| `deleteOldVersion` | 否 | 删除旧版本再建 |
| …… | | 其余字段参考 `ServiceInterface.ts::IUpdateMapParam` |

### 出参

结构与 `open_map` 返回一致。

### 示例

```python
svc.update_map("demo_dwg", "v1", body={
    "mapid": "demo_dwg",
    "fileid": up["fileid"],
    "uploadname": up["uploadname"],
    "mapopenway": "Memory",
    "description": "添加二次开发示例描述",
})
```

---

## 5. 关闭地图 `close_map`

**URL**：`GET /api/v1/map/cmd/closemap/{mapid}/{version}`  

仅断开 图形服务 中的内存/渲染引用；不删除数据库文件，也不删图。

### 出参

```json
{"status": true}
```

### 注意：POST 无 body 会 500

```bash
# ✗ 500 param error
curl -X POST ".../closemap/sys_zp/v1?token=$T"
# ✓ 200
curl ".../closemap/sys_zp/v1?token=$T"
# ✓ 200（POST 可以，但 body 必须有至少一个字段）
curl -X POST ".../closemap/sys_zp/v1?token=$T" -d '{"fileid":"caf27de8a225"}' -H 'content-type: application/json'
```

SDK 内部统一使用 GET 规避该陷阱。

---

## 6. 删除地图 `delete_map`

**URL**：`GET /api/v1/map/cmd/deletemap/{mapid}/{version}?keepSource={bool}`  

| 参数 | 默认 | 说明 |
|------|------|------|
| `keepSource` | `true` | `true` 保留上传的源文件（只清元数据/DB），`false` 同时删源文件 |

### 出参

```json
{"status": true}
```

### 示例

```python
svc.delete_map("demo_dwg", "v1", keep_source=True)
```

---

## 7. 重命名地图 `rename_map`（**已知不稳定**）

**URL**：`GET /api/v1/map/cmd/renamemap/{old}/_?newmapid={new}`  

### 响应样例（实测）

```json
{
  "status": false,
  "error": true,
  "reason": "mapid is empty",
  "newmapid": "xxx_new",
  "AuthorityId": "root"
}
```

---

## 8. 删除源文件 `delete_map_file`

**URL**：`GET /api/v1/map/cmd/deletemapfile/_/v1?files=a.dwg;b.dwg`  

删除上传到 `data/mapfiles/` 目录下的物理文件。被删的文件**必须没有图在引用**。

### 出参（实测）

```json
{
  "deleted": [],
  "deletedCount": 0,
  "notFound": ["__nb_nonexistent__.dwg"],
  "status": false
}
```

### 示例

```python
svc.delete_map_file(["caf27de8a225.dwg"])
# 或字符串
svc.delete_map_file("a.dwg;b.dwg")
```

---

## 9. 列出地图 `list_maps`

**URL**：`GET /api/v1/map/cmd/listmaps/_/v1`  

### 入参

| Query | 说明 |
|-------|------|
| `keyword` | 按 mapid 过滤（可选） |
| `page` / `pageSize` | 分页（可选，多数版本支持） |

### 出参（实测）

```json
{
  "sys_zp": [
    {
      "db": "c47be46a2ad2.db",
      "filename": "c47be46a2ad2.dwg",
      "geom": true,
      "mtime": "2026-04-10 11:31:00",
      "status": "finish",
      "uploadname": "sys_zp.dwg",
      "version": "v1"
    }
  ]
}
```

- 顶层 key 是 `mapid`，value 是该图所有版本的数组
- `geom:true` 表示该图已有切片缓存
- `status:"finish"` 表示打开流程完成

### 示例

```python
maps = svc.list_maps()
for mapid, versions in maps.items():
    print(mapid, [v["version"] for v in versions])
```

```javascript
const maps = await svc.listMaps();
```

---

## 10. 读元数据 `metadata`

**URL**：`GET /api/v1/map/cmd/metadata/{mapid}/{version}`  

图必须先 `open_map` 打开才能读到完整元数据。

### 入参（Query）

| 字段 | 说明 |
|------|------|
| `enableEditMeta` | `true` 时返回可编辑字段；默认 `false` |
| `fileid` | 罕见用法：读源文件级元数据 |

### 出参（节选）

字段非常多，以下是二次开发最常用的子集：

| 字段 | 类型 | 含义 |
|------|------|------|
| `mapid / name / version / fileid / uploadname / filename` | string | 定位 |
| `bounds` | string("[minx,miny,maxx,maxy]") | 整图范围 |
| `dbBounds` / `drawBounds` / `initBounds` / `viewBounds` | string | 各阶段范围 |
| `createTime` / `time` / `mtime` | string | 时间戳 |
| `layers` | JSON 字符串（数组） | 图层列表 |
| `styles` | JSON 字符串（数组） | 样式列表 |
| `layouts` | string，逗号分隔 | 布局名 |
| `findFonts` / `preferableFonts` | JSON/字符串 | 字体字典 |
| `geomRecordCount` | string | 几何记录总数（估值） |
| `ucsorg` | string | UCS 原点矩阵 |
| `pid` | string |  进程 PID（仅 GeomRender） |



> ⚠ 注意：`layers` / `styles` / `findFonts` 等被序列化成**字符串**存在顶层；SDK 不会自动解嵌套 JSON，请按需 `json.loads()`。

### 示例

```python
svc.open_map("sys_zp", "v1", mapopenway="Memory",
             fileid="c47be46a2ad2", uploadname="sys_zp.dwg")
meta = svc.metadata("sys_zp", "v1")
layers = json.loads(meta["layers"])
```

---

## 11. 修改元数据 `update_metadata`

**URL**：`POST /api/v1/map/cmd/updateMetadata/{mapid}/{version}`  

通过 POST body 修改图的"用户可编辑字段"（通常是 description、layerOn 状态、业务标签等）。

### 入参（JSON body）

```json
{
  "fields": [
    { "name": "description", "value": "我是新的描述" },
    { "name": "tags",        "value": "building,sample" }
  ]
}
```

具体可编辑字段清单取决于后端版本；二次开发前可用 `metadata?enableEditMeta=true` 先探测。

### 出参

```json
{"status": true}
```

### 示例

```python
svc.update_metadata("demo_dwg", "v1", body={
    "fields": [{"name": "description", "value": "hello"}]
})
```

---

## 12. 缩略图 `thumbnail`

**URL**：`GET /api/v1/map/cmd/thumbnail/{mapid}/{version}?width=W&height=H[&layers=...][&backcolor=...]`  

返回 PNG 图片字节流。

### 常用参数

| Query | 说明 |
|-------|------|
| `width` / `height` | 缩略图像素大小 |
| `layers` | 仅渲染指定图层；逗号分隔 |
| `backcolor` | 背景色（`0xRRGGBB` 数字或 `#RRGGBB`） |

### SDK

```python
svc.open_map("sys_zp", "v1", mapopenway="GeomRender",
             fileid="c47be46a2ad2", uploadname="sys_zp.dwg")
png = svc.thumbnail_bytes("sys_zp", "v1", width=256, height=160)
open("thumb.png","wb").write(png)
```

```javascript
const buf = await svc.thumbnailBytes('sys_zp', 'v1', { width: 256, height: 160 });
fs.writeFileSync('thumb.png', buf);
```

也可以只构造 URL 直接嵌到 `<img src>`：

```python
url = svc.thumbnail_url("sys_zp", "v1", width=128, height=96)
# => http://127.0.0.1:27660/api/v1/map/cmd/thumbnail/sys_zp/v1?width=128&height=96&token=...
```

---

## 13. 支持的格式 `support_format`

**URL**：`GET /api/v1/map/cmd/supportFormat/_/v1`  

### 实测响应

```json
{
  "cad": "dwg;dxf",
  "image": "webp;heic;zip;png;pdf;jpg;jpe;jpc;heif;exif;avif;bmp;dib;tiff;hif;hdr;jfif;gif;tif;jpeg"
}
```

---

## 14. 系统常量 `const_data`

**URL**：`GET /api/v1/map/cmd/constData/_/v1`  

返回系统级字典，最常用的是 **CAD 实体类型号 → AcDb 类名** 的映射。

### 实测响应（节选）

```json
{
  "entTypeIdMap": {
    "1":  "AcDbLine",
    "2":  "AcDbPolyline",
    "5":  "AcDbSpline",
    "6":  "AcDbArc",
    "7":  "AcDbCircle",
    "10": "AcDbBlockReference",
    "11": "AcDbHatch",
    "12": "AcDbMText",
    "13": "AcDbText",
    "34": "AcDb3dSolid"
  }
}
```

搭配 `query_features` 的 `entType` 过滤参数使用。

---

## 15. 数据范围 `get_data_bounds`

**URL**：`GET /api/v1/map/cmd/getDataBounds/{mapid}/{version}`  

返回图上所有 **几何实体**的紧凑包围盒，比 `metadata.bounds`（可能含空图框）更贴合可见数据。

### 实测响应

```json
{
  "bounds": [
    587464241.0047014,
    3103794303.1750364,
    587769759.9828022,
    3104004303.209077
  ],
  "status": true
}
```

### 示例

```python
b = svc.get_data_bounds("sys_zp", "v1")["bounds"]
# 用来设置 vjmap 初始 fitBounds
```

---

- 

> 下一分类：[`02-tiles-and-styles.md`](02-tiles-and-styles.md)

---

## api/02-tiles-styles.md
<a id=api02-tiles-stylesmd></a>

# 瓦片 / 样式

> 分类代号：C.2  本章共 **10 个接口**（3 个纯 URL 拼装 + 7 个 REST）

vjmap 的瓦片出图有两套机制：

1. **按地图内置默认 layer 出图**  
   `open_map` 之后，可直接用 `raster_tile_url()` / `vector_tile_url()` 拿到 `{z}/{x}/{y}` 占位符 URL，喂给 vjmap。
2. **按自定义 style 出图**  
   先 `create_map_style()` 拿到 `stylename`，把它作为 `layer` 段传给 tile URL，即可得到"按该 style 渲染"的瓦片。  
   后续可用 `cmd_update_style()` 原地修改；用 `cmd_switch_layers()` 临时切换可见图层；用 `slice_layer()` 触发预切片缓存；用 `get_slice_cache_zoom()` 查询当前已缓存到几级；用 `delete_style()` 清理。

WMS 出图走的是独立管线，用 `wms_tile_url()` 构造即可，单图走 `cmd/wms/{mapid}/{version}`，多图叠加走 `/service/wms`。

---

## 快速索引

| 序号 | SDK 方法 | HTTP | URL |
|----:|---------|------|-----|
| 16 | `raster_tile_url` / `rasterTileUrl` | GET | `/api/v1/map/tile/{mapid}/{version}/{layer}/{z}/{x}/{y}` |
| 17 | `vector_tile_url` / `vectorTileUrl` | GET | `/api/v1/map/tile/{mapid}/{version}/{layer}/{z}/{x}/{y}.mvt` |
| 18 | `wms_tile_url` / `wmsTileUrl`（单图） | GET | `/api/v1/map/cmd/wms/{mapid}/{version}` |
| 19 | `wms_tile_url` / `wmsTileUrl`（多图） | GET | `/api/v1/service/wms` |
| 20 | `create_map_style` / `createMapStyle` | POST | `/api/v1/map/cmd/createMapStyle/{mapid}/{version}` |
| 21 | `cmd_switch_layers` / `cmdSwitchLayers` | POST | `/api/v1/map/cmd/switchlayer/{mapid}/{version}` |
| 22 | `cmd_update_style` / `cmdUpdateStyle` | POST | `/api/v1/map/cmd/updateStyle/{mapid}/{version}` |
| 23 | `slice_layer` / `sliceLayer` | POST | `/api/v1/map/cmd/slicelayer/{mapid}/{version}` |
| 24 | `get_slice_cache_zoom` / `getSliceCacheZoom` | POST | `/api/v1/map/cmd/getSliceCacheZoom/{mapid}/{version}` |
| 25 | `delete_style` / `deleteStyle` | POST | `/api/v1/map/cmd/deletestyle/{mapid}/{version}` |

> **注意**：10/21/22/23/24/25 这 6 个是  `cmd` 路由，**POST 必须带非空 JSON body**（即使是 `{"noop":true}`），否则会返回 `500 param error`。SDK 已经替你处理好这点，直接调方法即可。

---

## 16 · 栅格瓦片 URL

**SDK**：`svc.raster_tile_url(mapid?, version?, layer?, fileid?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` | string | 可省，默认为当前 `_cur_map.mapid` |
| `version` | string | 可省，默认 `v1` |
| `layer` | string | 可省，默认 `_`（即图档内置默认 style）。若先调用 `create_map_style`，把返回的 `stylename` 传进来即可 |
| `fileid` | string | 可省。主要用于 tile 缓存破坏。后端不校验，只要 `{mapid,version,layer}` 对，tile 就出得来 |

### 实采示例

```json
"http://127.0.0.1:27660/api/v1/map/tile/sys_zp/v1/_/{z}/{x}/{y}?tag=c47be46a2ad2&token=<jwt>"
```

### Python

```python
from vjmap import Vjmap, MapOpenWay

svc = Vjmap("http://127.0.0.1:27660/api/v1", TOKEN)
svc.open_map(mapid="sys_zp", version="v1", mapopenway=MapOpenWay.GeomRender)
print(svc.raster_tile_url())
```

### Node.js

```javascript
import { Vjmap, MapOpenWay } from './sdk/nodejs/vjmap.js';

const svc = new Vjmap('http://127.0.0.1:27660/api/v1', TOKEN);
await svc.openMap({ mapid: 'sys_zp', version: 'v1', mapopenway: MapOpenWay.GeomRender });
console.log(svc.rasterTileUrl());
```

---

## 17 · 矢量瓦片 URL（MVT）

**SDK**：`svc.vector_tile_url(...)`，参数与 `raster_tile_url` 完全一致，区别是 URL 末尾多了 `.mvt` 后缀。

### 实采示例

```json
"http://127.0.0.1:27660/api/v1/map/tile/sys_zp/v1/main/{z}/{x}/{y}.mvt?tag=c47be46a2ad2&token=<jwt>"
```

`{layer}` 默认是 `main`（内置 MVT 默认名），不是 `_`。

### 用法

MVT 非常适合作为 vjmap `vector` source：

```javascript
map.addSource('vj', {
  type: 'vector',
  tiles: [svc.vectorTileUrl()],
  minzoom: 0,
  maxzoom: 22,
});
map.addLayer({ id: 'vj-line', source: 'vj', 'source-layer': 'default', type: 'line' });
```

---

## 18 / 19 · WMS 瓦片 URL

**SDK**：`svc.wms_tile_url(mapid, version?, *, layer?, width?, height?, bbox?, srs?, backcolor?, mvt?)`

### 参数规则

- `mapid`: 可以是字符串，也可以是 `list[str]`
  - **单图** → 走 `/api/v1/map/cmd/wms/{mapid}/{version}`
  - **多图** → 走 `/api/v1/service/wms?mapid=a;b;c&version=v1;v1;v1`
- `bbox`: 默认 `{bbox-epsg-3857}` 占位符。如果你直接用 如Leaflet，就保留占位符交给前端去替换。SDK 刻意**不对 `bbox` 做 percent-encode**。
- `srs`: 坐标系，默认 `EPSG:3857`。
- `mvt=True`: URL 末尾加 `.mvt`，输出矢量。
- `backcolor`: 背景色，十进制 RGB（0 = 黑/透明）。
- `layer`: 可选，指定某个 stylename 渲染。

### 实采示例

单图：

```json
"http://127.0.0.1:27660/api/v1/map/cmd/wms/sys_zp/v1?bbox={bbox-epsg-3857}&srs=EPSG%3A3857&width=512&height=512&token=<jwt>"
```

多图：

```json
"http://127.0.0.1:27660/api/v1/service/wms?bbox={bbox-epsg-3857}&mapid=sys_zp&version=v1&srs=EPSG%3A3857&width=256&height=256&token=<jwt>"
```

### Python

```python
url = svc.wms_tile_url("sys_zp", "v1", width=512, height=512)
url_multi = svc.wms_tile_url(["sys_zp", "other"], ["v1", "v1"])
```

### Node.js

```javascript
const url = svc.wmsTileUrl({ mapid: 'sys_zp', version: 'v1', width: 512, height: 512 });
const urlMulti = svc.wmsTileUrl({ mapid: ['sys_zp', 'other'], version: ['v1', 'v1'] });
```

---

## 20 · 创建地图样式 `createMapStyle`

**URL**：`POST /api/v1/map/cmd/createMapStyle/{mapid}/{version}`  
**SDK**：`svc.create_map_style(mapid, version, *, layeron, layeroff, backcolor, clipbounds, expression, lineweight, is_geom_layer=True, name)`  

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `geom` | bool | `true` 几何渲染（默认）；`false` 栅格渲染 |
| `name` | string | style 名字，可自动生成 |
| `layeron` | string / 数组 | 可见图层；数组传入时 SDK 会自动转成 `(0,1,2)` 格式 |
| `layeroff` | string / 数组 | 隐藏图层 |
| `backcolor` | int | 背景色（0=黑色，常用于暗色底图） |
| `clipbounds` | string | 裁剪范围 JSON 字符串 |
| `expression` | string | Cesium 风格的颜色表达式 |
| `lineweight` | int[] | 线宽缩放 `[默认,最小,最大]` |

### 响应（实采）

```json
{
  "stylename": "sfbd35705c"
}
```

返回的 `stylename` 就是后续作为瓦片 URL 中 `{layer}` 段的值。

### Python

```python
style = svc.create_map_style("sys_zp", "v1", layeron="*", backcolor=0)
tile_url = svc.raster_tile_url(layer=style["stylename"])
```

### Node.js

```javascript
const style = await svc.createMapStyle({ mapid: 'sys_zp', version: 'v1', layeron: '*', backcolor: 0 });
const tileUrl = svc.rasterTileUrl({ layer: style.stylename });
```

### curl

```bash
curl -X POST "http://127.0.0.1:27660/api/v1/map/cmd/createMapStyle/sys_zp/v1?token=$TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"geom":true,"layeron":"*","backcolor":0}'
```

---

## 21 · 切换可见图层 `switchlayer`

**URL**：`POST /api/v1/map/cmd/switchlayer/{mapid}/{version}`  
**SDK**：`svc.cmd_switch_layers(visible_layers, *, expression?, dark_mode?, mapid?, version?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `visibleLayer` | string[] | 要可见的图层 id 列表 |
| `expression` | string | 颜色表达式（与 createMapStyle 同） |
| `darkMode` | bool | 是否暗色（可在 openMap 时也指定） |

### 响应（实采）

```json
{
  "layerid": "s24d720029"
}
```

与 `createMapStyle` 的 `stylename` 相似，`layerid` 指向新的内部 style（后端按参数 hash 复用，不会无限增长）。

### Python

```python
res = svc.cmd_switch_layers(["0"])
print(res["layerid"])
```

### Node.js

```javascript
const res = await svc.cmdSwitchLayers(["0"]);
console.log(res.layerid);
```

---

## 22 · 原地修改样式 `updateStyle`

**URL**：`POST /api/v1/map/cmd/updateStyle/{mapid}/{version}`  
**SDK**：`svc.cmd_update_style(*, name, layeron, layeroff, backcolor, clipbounds, expression, lineweight, mapid?, version?)`

与 `createMapStyle` 最大的区别：**它可以修改已有 style（传同名 name）**，而不是每次新建。

### 响应（实采）

```json
{
  "bounds": "[587227463.27, 3103509765.63, 588006538.39, 3104288840.75]",
  "boundsChange": false,
  "layerid": "m765111cb4"
}
```

`boundsChange` = false 表示裁剪范围没变；若 `clipbounds` 修改了，`bounds` 与 `boundsChange` 会反映出来。

### Python

```python
svc.cmd_update_style(
    mapid="sys_zp", version="v1",
    name="nb_style_1",
    layeron="*",
    backcolor=0,
)
```

### Node.js

```javascript
await svc.cmdUpdateStyle({
  mapid: 'sys_zp', version: 'v1',
  name: 'nb_style_1',
  layeron: '*',
  backcolor: 0,
});
```

---

## 23 · 触发预切片缓存 `slicelayer`

**URL**：`POST /api/v1/map/cmd/slicelayer/{mapid}/{version}`  
**SDK**：`svc.slice_layer(*, layer, zoom, ismvt, iscancel, is_all_cancel, batch_num?, idle_batch_sleep_ms?, busy_batch_sleep_ms?, mapid?, version?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `layer` | string | 要预切的 layer id（即 stylename） |
| `zoom` | int / int[] / string | 要切的 zoom 层级。单个或数组 |
| `ismvt` | bool | true 切 MVT，false 切栅格 |
| `iscancel` | bool | 取消本次任务 |
| `isAllCancel` | bool | 取消该地图所有正在进行的切片任务 |
| `batchnum` | int | 批处理大小 |
| `idle_batch_sleepms` | int | 空闲批间隔毫秒 |
| `busy_batch_sleepms` | int | 忙时批间隔毫秒 |

### 响应（实采）

取消切片：

```json
{}
```

即空对象。真实触发切片时后端会流式推进度（`{progress:0.5,total:...}`），这里我们的 SDK 只取第一响应。

### Python

```python
svc.slice_layer(layer="s24d720029", zoom=[0, 1, 2, 3], ismvt=False)
```

### Node.js

```javascript
await svc.sliceLayer({ layer: 's24d720029', zoom: [0,1,2,3], ismvt: false });
```

---

## 24 · 查询已缓存的切片级别 `getSliceCacheZoom`

**URL**：`POST /api/v1/map/cmd/getSliceCacheZoom/{mapid}/{version}`  
**SDK**：`svc.get_slice_cache_zoom(*, layer, ismvt, mapid?, version?)`

### 响应（实采）

```json
{
  "sliceCacheZoom": 0
}
```

`sliceCacheZoom` = 当前已稳定缓存到的最大 zoom。若 = 0 通常表示还没跑过预切片。

### Python / Node

```python
print(svc.get_slice_cache_zoom(layer="s24d720029", ismvt=False))
```

```javascript
console.log(await svc.getSliceCacheZoom({ layer: 's24d720029', ismvt: false }));
```

---

## 25 · 删除自定义样式 `deletestyle`

**URL**：`POST /api/v1/map/cmd/deletestyle/{mapid}/{version}`  
**SDK**：`svc.delete_style(stylename, *, mapid?, version?)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `stylename` | string | 要删除的 style 名（即 `create_map_style` 返回的 `stylename`） |

### 响应（实采）

即便 stylename 不存在也返回空对象（幂等）：

```json
{}
```

### Python / Node

```python
svc.delete_style("sfbd35705c", mapid="sys_zp", version="v1")
```

```javascript
await svc.deleteStyle('sfbd35705c', { mapid: 'sys_zp', version: 'v1' });
```

---

## 组合示例：从开图到看地图

```python
from vjmap import Vjmap, MapOpenWay

svc = Vjmap("http://127.0.0.1:27660/api/v1", TOKEN)
svc.open_map(mapid="sys_zp", version="v1", mapopenway=MapOpenWay.GeomRender)

# 1. 自定义一个黑底白线风格
style = svc.create_map_style(
    "sys_zp", "v1",
    layeron="*",
    backcolor=0,
    expression="gOutColorRed:=255;gOutColorGreen:=255;gOutColorBlue:=255;gOutColorAlpha:=255;",
)

# 2. 拿 tile URL 给 vjmap
raster = svc.raster_tile_url(layer=style["stylename"])

# 3. 预切前 4 级，加快首屏
svc.slice_layer(layer=style["stylename"], zoom=[0,1,2,3], ismvt=False)

# 4. 不用了，清掉
svc.delete_style(style["stylename"], mapid="sys_zp", version="v1")
```

---

## api/03-query.md
<a id=api03-querymd></a>

# 要素查询 / 几何

> 分类代号：C.3  本章共 **4 个接口**

本章汇集所有"拿到几何 / 属性 / 投影"的基础操作。其中 `queryFeatures` 是最常用的接口，根据 `querytype` 可覆盖点/矩形/表达式/条件 4 种查询场景，因此 SDK 只暴露一个 `query_features(querytype=...)` 统一入口。

---

## 快速索引

| 序号 | SDK 方法 | HTTP | URL |
|----:|---------|------|-----|
| 26 | `query_features` / `queryFeatures` | POST | `/api/v1/map/cmd/queryFeatures/{mapid}/{version}` |
| 27 | `create_entity_geo_data` / `createEntityGeoData` | POST | `/api/v1/map/cmd/createEntityGeoData/_null/v1` |
| 28 | `coord_transform` / `coordTransform` | POST | `/api/v1/map/cmd/coordtransform/_/_` |
| 29 | `prj_wkt_to_prj4_str` / `prjWktToPrj4Str` | POST | `/api/v1/map/cmd/prjWktToPrj4Str/_/_` |

---

## 26 · 要素查询 `queryFeatures`

**URL**：`POST /api/v1/map/cmd/queryFeatures/{mapid}/{version}`  
**SDK**：`svc.query_features(querytype, ...)` / `svc.queryFeatures({ querytype, ... })`  

### `querytype` 枚举

| 值 | 含义 | 必填参数 |
|----|------|---------|
| `point` | 点查询：在屏幕像素范围内找要素 | `x`, `y`, `pixelsize`, `zoom`, `pixelToGeoLength` |
| `rect` | 矩形查询：几何 bounds 内找要素 | `x1`, `y1`, `x2`, `y2` |
| `expresion` | 表达式查询：按 DWG 属性字段过滤 | `expr` |
| `condition` | 条件查询：按后端预定义条件 | `condition`（可空），可选 `bounds` |

> ⚠️ `expresion` 是后端拼写，**不是** `expression`。

### 可选参数（所有 querytype 通用）

| 字段 | 说明 |
|------|------|
| `limit` | 最大返回条数（同时会写入 `maxReturnCount` 和 `limit` 两个字段） |
| `fields` | 要返回的属性字段，逗号分隔，空串 = 全部 |
| `geom` | 是否返回几何（默认 true，false 则只返回 envelop/attrs） |
| `simplifyTolerance` | 几何简化容差 |
| `useCache` | 开启查询缓存（condition/expresion 常用） |
| `toMapCoordinate` | 是否把返回坐标转成地图坐标系 |
| `maxGeomBytesSize` | 单条几何字节大小上限（超过则降级为 envelop） |

### 响应结构

```json
{
  "recordCount": 5,
  "result": [
    {
      "id": 1,
      "objectid": "EA_ED__EE_#",
      "layerindex": 13,
      "name": "AcDbArc",
      "geojson": "...",
      "envelop": "POLYGON(...)",
      "bounds": "[minx,miny,maxx,maxy]",
      "isEnvelop": true,
      "color": -65281,
      "linetype": "BYLAYER",
      "xdata": "",
      "center": "x,y,z",
      "points": "x1,y1,z1;x2,y2,z2;...",
      "colorIndex": 6
    }
  ]
}
```

- `recordCount`：**后端实际扫到的全量数量**，不是本次返回条数。想分页时根据 `recordCount` + `beginpos` 自行控制。
- `geojson` 是字符串而非对象，调用方需要再 `JSON.parse()`。
- `isEnvelop=true` 说明 geojson 被替换成了 envelop（单条太大或命中 `maxGeomBytesSize`）。

### 实采（rect 查询 sys_zp/v1，limit=5）

```json
{
  "recordCount": 5,
  "result": [{
    "id": 1,
    "objectid": "EA_ED__EE_#",
    "layerindex": 13,
    "name": "AcDbArc",
    "geojson": "{\"type\":\"GeometryCollection\",\"geometries\":[{\"type\":\"Polygon\",\"coordinates\":[[[2271440.45,-685837.25],[2271440.45,-679783.61],[2276451.33,-679783.61],[2276451.33,-685837.25],[2271440.45,-685837.25]]]}]}",
    "envelop": "POLYGON((2271440.45 -685837.25, ... ))",
    "bounds": "[587661158.58,3103885970.22,587661256.00,3103886087.91]",
    "isEnvelop": true,
    "color": -65281,
    "linetype": "BYLAYER"
  }]
}
```

### 实采（condition 查询，limit=1）

```json
{
  "recordCount": 6982,
  "result": [{
    "id": 1,
    "name": "AcDbArc",
    "geojson": "",
    "envelop": "POLYGON(...)",
    "bounds": "[...]",
    "isEnvelop": false,
    "color": -65281,
    "center": "587661197.39,3103886029.19,-152738.04",
    "points": "x1,y1,z1;x2,y2,z2;x3,y3,z3",
    "colorIndex": 6
  }]
}
```

`recordCount=6982` 表示该地图共 6982 个要素满足条件；`limit=1` 只取第一条。

### Python

```python
# 矩形查询：sys_zp/v1 全图前 5 条
meta = svc.open_map(mapid="sys_zp", version="v1", mapopenway=MapOpenWay.GeomRender)
x1, y1, x2, y2 = [float(x) for x in meta["bounds"][1:-1].split(",")][:4]
res = svc.query_features(
    querytype="rect",
    mapid="sys_zp", version="v1",
    x1=x1, y1=y1, x2=x2, y2=y2,
    limit=5,
)
for item in res["result"]:
    print(item["id"], item["name"])

# 点查询：屏幕 5 像素范围
res = svc.query_features(
    querytype="point",
    x=587661197.39, y=3103886029.19,
    zoom=10,
    pixelsize=5,
    pixel_to_geo_length=100.0,
    limit=3,
)
```

### Node.js

```javascript
const res = await svc.queryFeatures({
  querytype: 'rect',
  mapid: 'sys_zp', version: 'v1',
  x1, y1, x2, y2, limit: 5,
});

const byExpr = await svc.queryFeatures({
  querytype: 'expresion',
  expr: "layerindex=13",
  limit: 10,
});
```

### curl

```bash
curl -X POST "http://127.0.0.1:27660/api/v1/map/cmd/queryFeatures/sys_zp/v1?token=$TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"querytype":"condition","layername":"","zoom":1,"limit":1}'
```

---

## 27 · 构造实体几何 `createEntityGeoData`

**URL**：`POST /api/v1/map/cmd/createEntityGeoData/_null/v1`  
**SDK**：`svc.create_entity_geo_data(mapdata=..., map_extent=..., render_accuracy=..., exclude_attribute=..., use_zip=...)`

> `mapdata` 是前端 `@vj/draw` 构造的 DWG 结构体（JSON 字符串）。独立使用场景较少，通常和前端绘图配合。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapdata` | string | 前端传入的 DWG JSON 字符串 |
| `mapExtent` | string | 地图范围 JSON：`[minx,miny,maxx,maxy]` |
| `renderAccuracy` | number | 渲染精度（像素） |
| `excludeAttribute` | bool | 是否排除属性（默认 true） |
| `useZip` | bool | 是否对 mapdata 做 gzip 压缩（默认 true） |

### 响应

- 合法输入：`{ "result": [...], "bounds": "...", ... }`（与 queryFeatures 类似）

---

## 28 · 坐标转换 `coordtransform`

**URL**：`POST /api/v1/map/cmd/coordtransform/_/_`  
**SDK**：`svc.coord_transform(srs=..., crs=..., points=..., four_parameter=..., is_inverse_four_parameter=...)`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `srs` | string | 源坐标系，例如 `EPSG:4326` |
| `crs` | string | 目标坐标系，例如 `EPSG:3857` |
| `points` | string | 多点串：`x1,y1;x2,y2` |
| `fourParameter` | string | 可选四参数：`xOff,yOff,scale,rotRad` |
| `isInverseFourParamter` | bool | 是否反向应用四参数（注意后端拼写是 `Paramter`） |

SDK 的 `points` 接受：

- 单点 `(x,y)` 或 `{"x":1,"y":2}`
- 多点 `[(x1,y1),(x2,y2)]`
- `fourParameter` 支持 `"x,y,s,r"` 或 `[x,y,s,r]`

### 响应（实采：4326 → 3857 两点）

```json
{
  "points": [
    [12957254.769864663, 4852582.082864688],
    [13522423.824622115, 3662655.1833045874]
  ]
}
```

### Python

```python
res = svc.coord_transform(
    srs="EPSG:4326",
    crs="EPSG:3857",
    points=[(116.397, 39.908), (121.474, 31.23)],
)
print(res["points"])  # [[12957254.77, 4852582.08], [13522423.82, 3662655.18]]
```

### Node.js

```javascript
const res = await svc.coordTransform({
  srs: 'EPSG:4326',
  crs: 'EPSG:3857',
  points: [[116.397, 39.908], [121.474, 31.23]],
});
console.log(res.points);
```

---

## 29 · WKT → PROJ4 `prjWktToPrj4Str`

**URL**：`POST /api/v1/map/cmd/prjWktToPrj4Str/_/_`  
**SDK**：`svc.prj_wkt_to_prj4_str(wkt, from_="")`

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `wkt` | string | ESRI prj 文件内容或 WKT 字符串 |
| `from` | string | 数据来源。空串 = WKT；其他可选 `wmsauto / xml / urn / crsurl / url / micoordsys / pci` |

### 响应（实采：WGS84 WKT）

```json
{ "proj4": "+proj=longlat +datum=WGS84 +no_defs" }
```

### Python

```python
wkt = open("sample.prj", encoding="utf-8").read()
res = svc.prj_wkt_to_prj4_str(wkt)
print(res["proj4"])
```

### Node.js

```javascript
import { readFileSync } from 'node:fs';
const wkt = readFileSync('sample.prj', 'utf8');
const res = await svc.prjWktToPrj4Str(wkt);
console.log(res.proj4);
```

---

---

## api/04-analysis.md
<a id=api04-analysismd></a>

# 图形分析

> 分类代号：C.4  本章共 **6 个接口**

这一组接口都是 vjmap 的"增值分析"能力，参数差异大、且对输入的 DWG/图像质量要求很高，因此 SDK 采用 **pass-through** 风格：你按下面文档组参数，SDK 负责 POST 与错误处理，**不做参数字段级强校验**。

全部走 `POST /api/v1/map/cmd/{cmd}/_null/v1`（`_null` 表示不绑定具体 mapid，参数里自带）。

---

## 快速索引

| 序号 | SDK 方法 | cmd |
|----:|---------|------|
| 30 | `cmd_map_diff` / `cmdMapDiff` | `mapDiff` |
| 31 | `cmd_map_compare` / `cmdMapCompare` | `mapCompare` |
| 32 | `cmd_extract_table` / `cmdExtractTable` | `extractTable` |
| 33 | `cmd_extract_center_lines` / `cmdExtractCenterLines` | `extractCenterLines` |
| 34 | `cmd_match_object` / `cmdMatchObject` | `objectMatch` |
| 35 | `cmd_image_svg_to_dwg` / `cmdImageSvgToDwg` | `imageSvgToDwg` |

---

## 30 · 像素差异 `mapDiff`

比较两张 DWG **渲染成图片后的像素差异**，返回增删改的像素块外包矩形中心点。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid1` / `version1` | string | 旧图 |
| `mapid2` / `version2` | string | 新图 |
| `darkMode` | bool | 是否使用暗色底渲染 |
| `bufferSize` | int | 像素容差，相同像素在此距离内不判为"改" |
| `mergeClose` | bool | 合并靠得很近的差异块 |

### 响应（实采：同图 vs 同图）

```json
{
  "del": "",
  "modify": "",
  "new": "",
  "status": true
}
```

有差异时 `del` / `modify` / `new` 是 `x1,y1,x2,y2;x3,y3,x4,y4` 格式的字符串；`svc.cmd_map_diff()` 不做 parse，调用方按需切分。

### Python

```python
res = svc.cmd_map_diff(
    mapid1="sys_zp", version1="v1",
    mapid2="sys_zp", version2="v2",
    bufferSize=5,
)
```

---

## 31 · 实体对比 `mapCompare`

和 `mapDiff` 不同：`mapCompare` 比较的是 **DWG 实体**，不是渲染像素。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid1` / `version1`, `mapid2` / `version2` | string | 两张图 |
| `tolerancePos` | number | 坐标容差 |
| `toleranceAngle` | number | 角度容差（度） |



后端会先做一次快速 hash 去重。真实对比时返回：

```json
{
  "status": true,
  "add": [...],       // 仅在新图存在的实体 id
  "del": [...],       // 仅在旧图存在
  "modify": [...]     // 两边都有但有差异
}
```

### Python

```python
svc.cmd_map_compare(mapid1="a", version1="v1", mapid2="a", version2="v2")
```

---

## 32 · 表格提取 `extractTable`

自动从 DWG 里识别"线条围出来的表格"，返回单元格网格与每格文本。**最常用**于检图、清单导出。

### 请求参数

| 字段 | 说明 |
|------|------|
| `mapid` / `version` | 要识别的图 |
| `layer` | 可选，只在某 style / layer 上跑 |
| `bbox` | 可选，限定在地图坐标系下的 bbox |
| `minCellWidth` / `minCellHeight` | 最小单元格尺寸阈值 |

### 响应（实采：sys_zp/v1，无额外参数）

```json
{
  "tables": [{
    "attr": {
      "cellEmptyRatio": 45,
      "tableCellMaxCount": 70,
      "tableTextCount": 39,
      "unLinkLineRatio": 0
    },
    "colCount": 4,
    "cols": ["587693680.21", "587698180.22", "587717259.64", "..."],
    "datas": [["类目", "数量", "价格", "说明"], [...]],
    "rows": ["...", "..."]
  }]
}
```

- `cols` / `rows`：单元格网格线的坐标；
- `datas[i][j]`：每格文本（可能含编码乱码，视 DWG 字体而定）。

### Python

```python
res = svc.cmd_extract_table(mapid="sys_zp", version="v1")
for t in res.get("tables", []):
    print(t["colCount"], "x", len(t["rows"]))
```

### Node.js

```javascript
const res = await svc.cmdExtractTable({ mapid: 'sys_zp', version: 'v1' });
console.log(res.tables?.length);
```

---

## 33 · 中心线提取 `extractCenterLines`

对闭合多边形（如道路、管线）抽取中心线。

### 请求参数

| 字段 | 说明 |
|------|------|
| `mapid` / `version` | 源图 |
| `layer` | 可选，限定 layer |
| `expr` | 可选，按属性过滤实体（与 queryFeatures 的 expr 同义） |

### 响应（实采：sys_zp/v1）

```json
{
  "centerlines": [
    [[587562497.79, 3103857885.77], [587556239.69, 3103860401.38]],
    [[587603886.22, 3103921932.46], [587602627.19, 3103925333.77]]
  ]
}
```

`centerlines` 是"多段折线数组"；每条线由若干 `[x, y]` 顶点构成。

### Python

```python
res = svc.cmd_extract_center_lines(mapid="sys_zp", version="v1")
for line in res["centerlines"]:
    print("line with", len(line), "verts")
```

---

## 34 · 目标匹配 `objectMatch`

在底图中找与模板图形相似的子图（样式 + 尺寸）。常用于批量识别图章、标注块、设备。

### 请求参数

| 字段 | 类型 | 默认值 | 说明 |
|------|------|-------|------|
| `mapid` / `version` | string | — | 底图 |
| `templateMapid` / `templateVersion` | string | — | 模板图 |
| `objectBounds` | string / number[] | — | **必填**：模板所在的 bbox（地图坐标） `[x1,y1,x2,y2]` |
| `score` | number | 0.6 | 相似度阈值 |
| `canOverlap` | bool | false | 命中框是否允许重叠 |
| `maxOverlap` | number | 0.3 | 允许的最大重叠比例 |
| `toleranceAngle` | number | 180 | 角度容差（度），180 = 任意旋转 |
| `minReduceArea` | number | 256 | 小于此面积的命中框被忽略 |

### 响应


成功时返回：

```json
{
  "status": true,
  "matches": [
    { "bounds":"[x1,y1,x2,y2]", "angle": 0.0, "score": 0.92 },
    ...
  ]
}
```

### Python

```python
res = svc.cmd_match_object(
    mapid="sys_zp", version="v1",
    templateMapid="sys_zp", templateVersion="v1",
    objectBounds="[587661000,3103886000,587661200,3103886200]",
    score=0.7, toleranceAngle=90,
)
```

---

## 35 · SVG / 图像转 DWG `imageSvgToDwg`

把 SVG 字符串或位图转换成 DWG 实体。对位图会先做线条检测 + 矢量化。

### 请求参数

| 字段 | 说明 |
|------|------|
| `svgData` | SVG 文本 |
| `imageUrl` | 远程图片 URL（与 svgData 二选一） |
| `threshold` | 位图二值化阈值（0–255） |
| `mapid` / `version` | 可选：把结果直接写入该地图；为空则只返回 DWG JSON |

### 响应（实采：svgData="`<svg></svg>`"，空内容）

```json
{
  "error": "image or svg content is empty",
  "status": false
}
```

合法输入时返回：

```json
{
  "status": true,
  "mapdata": "<DWG JSON 字符串>"
}
```

可以把 `mapdata` 传给 `create_entity_geo_data`（见 [03-query.md](./03-query.md)）渲染。

---

## 通用注意

1. 这组接口都可能 **耗时 >10s**，SDK 默认 timeout=60s，建议在业务层再做重试与超时兜底。
2. 后端遇到参数非法时常返回 `{"error": "...", "status": false}`——不是 HTTP 4xx/5xx。SDK 会把它当成 2xx 正常响应返回给调用方。
3. 返回 `"status": false` 时 `error` 字段的语言**依系统 locale 而定**（我们这里抓到了中/英混合的错误串）。

---

---

## api/05-compose-export.md
<a id=api05-compose-exportmd></a>

# 组合 / 拆分 / 导出

> 分类代号：C.5  本章共 **6 个接口**

把多张 DWG 组合成一张、把一张大图按图框 / 图层拆成多张、把图纸导出为 PDF / DXF 或离线瓦片包——这一章就是"图纸经纪人"。

**共同特点**：这 6 个接口都相对重，后端会真实创建新图档 / 新文件。测试环境请用临时 mapid，不要对生产图做实验。

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 36 | `cmd_compose_new_map` / `cmdComposeNewMap` | `POST /api/v1/map/cmd/composeNewMap/_null/v1` |
| 37 | `cmd_split_map` / `cmdSplitMap` | `POST /api/v1/map/cmd/cmdSplitMap/_null/v1` |
| 38 | `cmd_export_layout` / `cmdExportLayout` | `POST /api/v1/map/cmd/exportLayout/_null/v1` |
| 39 | `cmd_export_pdf` / `cmdExportPdf` | `POST /api/v1/map/cmd/exportPdf/{mapid}/{version}` |
| 40 | `cmd_export_dxf` / `cmdExportDxf` | `POST /api/v1/map/cmd/exportDxf/{mapid}/{version}` |
| 41 | `cmd_save_offline_tile_db` / `cmdSaveOfflineTileDb` | `POST /api/v1/map/cmd/saveOfflineTileDb/_null/v1` |


---

## 36 · 组合新地图 `composeNewMap`

把多张已有 DWG 按几何变换（偏移 + 旋转）拼成一张。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `maps` | array | 组合清单：`[{mapid, version, dx?, dy?, angle?}]` |
| `newMapid` | string | 新图 mapid |
| `newVersion` | string | 新图 version（可省） |
| `bounds` | string | 可选：组合后的 bbox 限定 |

### 响应

成功：`{ "status": true, "mapid": "...", "fileid": "...", "version": "v1" }`  
失败（实采：源文件不存在）：

```json
{
  "error": "file is not exist, ",
  "status": false
}
```

### Python

```python
svc.cmd_compose_new_map(
    maps=[
        {"mapid": "part_a", "version": "v1", "dx": 0,      "dy": 0},
        {"mapid": "part_b", "version": "v1", "dx": 1000.0, "dy": 0, "angle": 0},
    ],
    newMapid="assembled_map",
)
```

### Node.js

```javascript
await svc.cmdComposeNewMap({
  maps: [
    { mapid: 'part_a', version: 'v1', dx: 0, dy: 0 },
    { mapid: 'part_b', version: 'v1', dx: 1000, dy: 0, angle: 0 },
  ],
  newMapid: 'assembled_map',
});
```

---

## 37 · 拆分子图 `cmdSplitMap`

按图框线 / 按图层把一张大图拆成多张小图。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` / `version` | string | 源图 |
| `splitType` | string | `frame`（按图框）/ `layer`（按图层） |
| `framelayer` | string | 仅 splitType=frame 时：图框线所在图层 |
| `clipBounds` | string | 也可以直接给一系列 bbox 列表拆 |
| `outNamePrefix` | string | 新图 mapid 前缀，默认用源 mapid + 序号 |

### 响应

成功：`{ "status": true, "maps": [{mapid, version, bounds}, ...] }`  
失败（实采：frame 拆分未给 clipBounds）：

```json
{ "error": "clipBounds is empty" }
```

---

## 38 · 导出布局为 DWG `exportLayout`

从 DWG 的**布局空间**（paper space）单独导出一张新 DWG。CAD 里经常一个文件多个布局（A3、A2、首页），这个接口能一次性把某个布局变成独立图档。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` / `version` | string | 源图 |
| `layoutIndex` | int | 布局序号（0 起） |
| `layoutName` | string | 或直接用布局名，优先级高于 `layoutIndex` |
| `newMapid` | string | 可选：直接导入为新地图 |
| `newVersion` | string | 可选：新地图 version |

### 响应（实采：sys_zp/v1, layoutIndex=0）

```json
{
  "fileid": "pf68817b9655",
  "status": true
}
```

`fileid` 指向后端缓存里的新 DWG 文件，可以接着调用 `open_map(mapid=...)` 打开，或用 `exportDxf / exportPdf` 进一步出图。

### Python

```python
res = svc.cmd_export_layout(mapid="sys_zp", version="v1", layoutIndex=0)
assert res["status"]
print(res["fileid"])  # 'pf68817b9655'
```

---

## 39 · 导出 PDF `exportPdf`

把指定图纸输出为 PDF。支持按地图 bbox 截图 + 指定纸张。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `bounds` / `bbox` | string | 要导出的 bbox：`[minx,miny,maxx,maxy]` |
| `width` / `height` | number | 纸张尺寸（mm），常用 A4=`297×210` |
| `scale` | number | 比例尺（可选） |
| `darkMode` | bool | 是否暗色渲染 |
| `outName` | string | 自定义输出文件名 |
| `layer` | string | 仅导出某 style / layer |

### 响应（实采：sys_zp/v1, 297×210 mm）

```json
{
  "path": "download/d5f939154c.pdf",
  "status": true
}
```

完整下载 URL 是 `http://127.0.0.1:27660/{path}?token=<jwt>`。

### Python

```python
res = svc.cmd_export_pdf(
    mapid="sys_zp", version="v1",
    width=297, height=210,
)
url = svc.base_gateway() + res["path"] + f"?token={svc.token}"
```

### Node.js

```javascript
const res = await svc.cmdExportPdf({
  mapid: 'sys_zp', version: 'v1',
  width: 297, height: 210,
});
const url = `${svc.baseGateway()}${res.path}?token=${svc.token}`;
```

---

## 40 · 导出 DXF `exportDxf`

把当前图档（或其子集）导出为 ASCII DXF 文件。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `bounds` / `bbox` | string | 裁剪 bbox |
| `layer` | string | 限定 style / layer |
| `dxfVersion` | string | DXF 版本 tag：`AC1009` (R12), `AC1018` (R2004) 等 |
| `outName` | string | 自定义输出文件名 |

### 响应

成功：`{ "status": true, "path": "download/xxx.dxf" }`  
失败（实采：当前地图没有可导出图元）：

```json
{
  "error": "Invalid Symbol Table name",
  "status": false
}
```

### Python

```python
res = svc.cmd_export_dxf(
    mapid="sys_zp", version="v1",
    dxfVersion="AC1018",
    bounds="[587661158,3103885970,587661256,3103886087]",
)
```

---

## 41 · 离线瓦片包 `saveOfflineTileDb`

把指定地图范围预切并打成 `.mbtiles` / `.sqlitedb` 离线包，方便前端离线展示。

### 请求参数

| 字段 | 类型 | 说明 |
|------|------|------|
| `mapid` / `version` | string | 源图 |
| `minzoom` / `maxzoom` | int | zoom 范围 |
| `bounds` | string | 要切的 bbox |
| `ismvt` | bool | true 切 MVT（.mbtiles），false 切栅格 PNG |
| `outName` | string | 输出文件名 |

### 响应

成功：`{ "status": true, "path": "download/xxx.mbtiles" }`  
失败（实采：缺 `bounds`）：

```json
{ "error": "param error" }
```

### Python

```python
res = svc.cmd_save_offline_tile_db(
    mapid="sys_zp", version="v1",
    minzoom=0, maxzoom=6,
    bounds="[587661158,3103885970,587661256,3103886087]",
    ismvt=True,
    outName="sys_zp_offline.mbtiles",
)
```

---

## 组合示例：DWG → PDF 一把梭

```python
# 1. 上传并打开 DWG
up = svc.upload_map("project.dwg", mapid="proj", version="v1")
svc.open_map(
    mapid="proj", version="v1",
    fileid=up["fileid"], uploadname=up["uploadname"],
    mapopenway=vjmap.MapOpenWay.GeomRender,
)

# 2. 导出第 0 号布局
lay = svc.cmd_export_layout(mapid="proj", version="v1", layoutIndex=0,
                             newMapid="proj_layout0", newVersion="v1")

# 3. 打开刚生成的布局图并导 PDF
svc.open_map(mapid="proj_layout0", version="v1",
             fileid=lay["fileid"], uploadname="proj_layout0.dwg",
             mapopenway=vjmap.MapOpenWay.GeomRender)
pdf = svc.cmd_export_pdf(mapid="proj_layout0", version="v1",
                          width=297, height=210)
print(svc.base_gateway() + pdf["path"])
```

---

---

## api/06-webcad.md
<a id=api06-webcadmd></a>

# WebCAD（条件启用）

> 分类代号：C.6  本章共 **6 个接口**

---

## 启用判定

最推荐的方式：

```python
svc = vjmap.Vjmap(API, TOKEN)
if svc.webcad_enabled():
    draws = svc.list_webcad_draws()
else:
    print("本服务未启用 WebCAD")
```

`svc.webcad_enabled()` 的判定规则：

1. `GET /api/v1/service/webcad` 若返回 2xx 且 **没有** `enabled=false` / `error` 字段，视为"可用"；
2. 若返回 4xx/5xx 或业务错误（如 `Token is invalid`），视为"不可用"。

### 本仓库实采

```json
// /api/v1/service/webcad?token=...
{
  "status": 403,
  "body": {
    "code": 7,
    "data": { "reload": true },
    "msg": "Token is invalid"
  }
}
```

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 42 | `list_webcad_draws` / `listWebcadDraws` | `GET /api/v1/map/cmd/listWebcadDraws/_/v1` |
| 43 | `get_webcad_data` / `getWebcadData` | `POST /api/v1/map/cmd/getWebcadData/{mapid}/{version}` |
| 44 | `save_webcad_patch` / `saveWebcadPatch` | `POST /api/v1/map/cmd/saveWebcadPatch/{mapid}/{version}` |
| 45 | `delete_webcad_draw` / `deleteWebcadDraw` | `POST /api/v1/map/cmd/deleteWebcadDraw/{mapid}/{version}` |
| 46 | `convert_webcad` / `convertWebcad` | `POST /api/v1/map/cmd/convertWebCAD/{mapid}/{version}` |
| 47 | `export_webcad` / `exportWebcad` | `POST /api/v1/map/cmd/exportWebCAD/_null/v1` |

---

## 42 · 列出 WebCAD 图 `listWebcadDraws`

**URL**：`GET /api/v1/map/cmd/listWebcadDraws/_/v1`  
**响应**（启用时）：

```json
{
  "draws": [
    { "mapid": "proj_wc", "version": "v1", "updatedAt": "2024-..." },
    ...
  ]
}
```

### Python / Node

```python
svc.list_webcad_draws()
```

```javascript
await svc.listWebcadDraws();
```

---

## 43 · 读取 WebCAD 数据 `getWebcadData`

**URL**：`POST /api/v1/map/cmd/getWebcadData/{mapid}/{version}`

### 请求参数

| 字段 | 说明 |
|------|------|
| `branch` | 分支名 |
| `includePatches` | 是否把历次补丁一并返回 |

### 响应（启用时）

```json
{
  "mapdata": "<DWG JSON 字符串>",
  "branch": "main",
  "headVersion": 12,
  "patches": []
}
```

### Python

```python
svc.get_webcad_data("proj_wc", "v1", branch="main")
```

---

## 44 · 保存 WebCAD 补丁 `saveWebcadPatch`

**URL**：`POST /api/v1/map/cmd/saveWebcadPatch/{mapid}/{version}`

### 请求参数

| 字段 | 说明 |
|------|------|
| `patch` | 一段 JSON/字符串形式的差异描述 |
| `branch` | 分支名 |
| `author` | 作者标识（可选） |
| `comment` | 提交说明 |
| `baseVersion` | 基于哪个补丁号提交，用于冲突检测 |

### 响应（启用时）

```json
{ "status": true, "version": 13 }
```

### Python

```python
svc.save_webcad_patch(
    "proj_wc", "v1",
    patch='[{"op":"add","entity":{...}}]',
    branch="main",
    author="alice",
    comment="add hatch",
    baseVersion=12,
)
```

---

## 45 · 删除 WebCAD 图 `deleteWebcadDraw`

**URL**：`POST /api/v1/map/cmd/deleteWebcadDraw/{mapid}/{version}`

### 请求参数

| 字段 | 说明 |
|------|------|
| `branch` | 只删某个分支；为空则删整图 |

### 响应（启用时）

```json
{ "status": true }
```

---

## 46 · 转为 WebCAD `convertWebCAD`

把一张已上传的 DWG 转成 WebCAD 可用的结构（初始化分支、预计算可编辑实体）。

**URL**：`POST /api/v1/map/cmd/convertWebCAD/{mapid}/{version}`

### 响应（启用时）

```json
{
  "status": true,
  "branch": "main",
  "entityCount": 12345
}
```

---

## 47 · 导出 WebCAD 为 DWG `exportWebCAD`

**URL**：`POST /api/v1/map/cmd/exportWebCAD/_null/v1`

### 请求参数

| 字段 | 说明 |
|------|------|
| `mapid` / `version` | 要导出的 WebCAD 图 |
| `branch` | 导出哪个分支（默认 main） |
| `newMapid` / `newVersion` | 新 DWG 的 mapid / version |

### 响应（启用时）

```json
{ "status": true, "path": "download/xxx.dwg" }
```

---

## api/07-workspace.md
<a id=api07-workspacemd></a>

# 工作区

> 分类代号：C.7  本章共 **5 个接口**

> 如果还没读过，建议先看 [入门 · 工作区](../04-workspace.md)，了解 vjmap 的命名空间模型。

工作区用来把地图 / KV / 全文搜索按"项目"隔开。默认所有地图都在"匿名工作区"；创建命名工作区后，URL 前缀由 `/api/v1/map/...` 变成 `/api/v1/workspace/{name}/...`。

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 48 | `get_workspaces` / `getWorkspaces` | `GET /api/v1/service/getWorkspace` |
| 49 | `workspace_create` / `workspaceCreate` | `POST /api/v1/service/workspaceCreate` |
| 50 | `workspace_modify` / `workspaceModify` | `POST /api/v1/service/workspaceModify` |
| 51 | `workspace_delete` / `workspaceDelete` | `DELETE /api/v1/service/workspaceDelete/{name}` |
| 52 | `workspace_status` / `workspaceStatus` | `GET /api/v1/service/workspaceStatus` |


---

## 48 · 列出工作区 `getWorkspace`

**URL**：`GET /api/v1/service/getWorkspace`

- 非 root 账户仅看到 **自己创建的** + **isPublic=true** 的工作区
- root（`AuthorityId:root`）账户看全部

### 响应（实采：创建 1 个后）

```json
[
  {
    "name": "nb_ws_1776831982",
    "alias": "nb 临时测试",
    "isPublic": false,
    "workDir": "nb_ws_1776831982",
    "idleCloseTime": 0,
    "disableFullSearch": false,
    "createTime": "2026-04-22 12:26:22"
  }
]
```

一个工作区都没有时返回 `null`（而不是空数组 `[]`）。

### Python

```python
ws_list = svc.get_workspaces() or []
for w in ws_list:
    print(w["name"], w["alias"])
```

### Node.js

```javascript
const ws = (await svc.getWorkspaces()) || [];
for (const w of ws) console.log(w.name, w.alias);
```

---

## 49 · 创建工作区 `workspaceCreate`

**URL**：`POST /api/v1/service/workspaceCreate`

### 请求 body

| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 唯一工作区名（**会出现在 URL 路径**里，注意字符集） |
| `alias` | string | 显示名，任意字符 |
| `workDir` | string | 工作区数据目录名；空串表示与 `name` 一致 |
| `isPublic` | bool | 是否对所有登录用户可见 |
| `disableFullSearch` | bool | 是否禁用该工作区的全文搜索索引 |

### 响应（实采）

```json
{
  "data": "nb_ws_1776831982",
  "status": "ok"
}
```

### Python

```python
svc.workspace_create(
    name="myproj",
    alias="我的项目",
    is_public=False,
)
```

### Node.js

```javascript
await svc.workspaceCreate({ name: 'myproj', alias: '我的项目' });
```

---

## 50 · 修改工作区 `workspaceModify`

**URL**：`POST /api/v1/service/workspaceModify`

body 必须是 `getWorkspaces()` 返回的**完整 obj**（包含 `createTime` 在内所有字段）。常见用法：先拿再改再 POST。

### 响应（实采）

```json
{
  "name": "nb_ws_1776831982",
  "status": "success"
}
```

### Python

```python
ws_list = svc.get_workspaces() or []
me = next(w for w in ws_list if w["name"] == "myproj")
me["alias"] = "新别名"
me["disableFullSearch"] = True
svc.workspace_modify(me)
```

### Node.js

```javascript
const ws = (await svc.getWorkspaces()) || [];
const me = ws.find(w => w.name === 'myproj');
me.alias = '新别名';
await svc.workspaceModify(me);
```

---

## 51 · 删除工作区 `workspaceDelete`

**URL**：`DELETE /api/v1/service/workspaceDelete/{name}`

### 响应（实采）

```json
{
  "name": "nb_ws_1776831982",
  "status": "success"
}
```

> ⚠️ 删除**不会**自动清理工作区内的地图数据；如需彻底清除，先在该工作区内删完地图再删工作区。SDK 切换到目标工作区调 `delete_map / delete_map_file` 即可（见 [01-map-management](./01-map-management.md)）。

### Python

```python
svc.workspace_delete("myproj")
```

### Node.js

```javascript
await svc.workspaceDelete('myproj');
```

---

## 52 · 工作区状态 `workspaceStatus`

**URL**：`GET /api/v1/service/workspaceStatus`

查询当前**正在服务中的工作区**（已启用 fork 进程 / 加载了地图）。若无工作区处于活跃状态，返回 `null`。

### 响应（实采：无活跃工作区）

```json
null
```

活跃时形如：

```json
[
  { "name": "myproj", "status": "running", "pid": 12345, "memory": 128_000_000 }
]
```

---

## 在 SDK 中切换工作区

SDK 构造后，可以用 `svc.use_workspace("myproj")` 把后续所有 `map/...` / `cmd/...` 调用都前缀到 `/workspace/myproj/`：

```python
svc = vjmap.Vjmap(API, TOKEN)
svc.use_workspace("myproj")
svc.list_maps()            # -> /api/v1/workspace/myproj/cmd/listmaps/...
svc.use_workspace(None)    # 切回匿名
```

Node.js 同名方法 `svc.useWorkspace("myproj")`。

---

---

## api/08-data-storage.md
<a id=api08-data-storagemd></a>

# 键值数据存储 (KV)

> 分类代号：C.8  本章共 **5 个接口**

vjmap 内置了一个按工作区隔离的 KV 数据表（SQLite），用来存用户配置、缩略图、缓存数据等零碎东西。"零碎"并不代表"不好用"——二开里非常常用，比如存每个用户的图层可见性、自定义地图标注等。

**存储位置**：后端 `data/customData.db`。  
**自动前缀**：所有用户可见 key 都加了隐形 `data_` 前缀，本 SDK 已屏蔽此细节。  
**TTL**：可选过期时间（秒），到期自动清理。  
**批量 key 分隔符**：路径里 `;;`（双分号）分隔多个 key。

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 53 | `get_custom_data` / `getCustomData` | `GET /api/v1/service/data/{key}` |
| 54 | `save_custom_data` / `saveCustomData` | `POST /api/v1/service/data/{key}` |
| 55 | `delete_custom_data` / `deleteCustomData` | `DELETE /api/v1/service/data/{key}` |
| 56 | `save_multiple_custom_data` / `saveMultipleCustomData` | `POST /api/v1/service/datas` |
| 57 | `get_custom_data_keys` / `getCustomDataKeys` | `GET /api/v1/service/datakeys?prefix=` |


---

## 53 · 获取值 `data/{key}` GET

### 查询参数

| 字段 | 取值 | 说明 |
|------|------|------|
| `retDataType` | `""` / `"value"` / `"prop"` | 默认返回 `{data, prop}`；`value` 只要 `data`；`prop` 只要 `prop` |
| `contentType` | `""` / `"image"` | `image` 时后端把 base64 dataURL 解出，直接返回 PNG 二进制（不是 JSON）|

批量：`key` 路径段用 `;;` 拼接多 key，SDK 传字符串列表即可。

### 响应

- 存在：`{ "status": "ok", "data": <value>, "prop": <prop> }`
- 不存在：`{ "status": "keyNoExist" }`（**不是 404**）
- 批量：`{ "values": [...], "props": [...], "status": [...] }`

### 实采

```json
// get_custom_data.json
{
  "data": "hello-vjmap",
  "prop": "text/plain",
  "status": "ok"
}
```

```json
// get_missing.json —— 一定要判断 status
{ "status": "keyNoExist" }
```

### Python

```python
d = svc.get_custom_data("myproj/theme")
if d["status"] == "keyNoExist":
    ...
else:
    theme = d["data"]
```

### Node.js

```javascript
const d = await svc.getCustomData('myproj/theme');
if (d.status === 'keyNoExist') { /* ... */ }
```

### 批量读

```python
svc.get_custom_data(["a", "b"])
# { "values": ["va","vb"], "props":["pa","pb"], "status":["ok","ok"] }
```

---

## 54 · 保存值 `data/{key}` POST

### 查询参数

| 字段 | 说明 |
|------|------|
| `ttl` | 秒；到点清除。省略或 `0` = 长期 |

### Body 两种写法

1. **JSON**：`{"value": "...", "prop": "..."}`；`prop` 可存元数据（MIME、扩展字段）
2. **Raw**：直接把文件二进制 POST 上去，作为 value 字段存（SDK 里叫 `raw_body` / `rawBody`）

### 响应（实采）

```json
{
  "data": "nb_kv_a_1776832185",
  "status": "ok"
}
```

### Python

```python
svc.save_custom_data("myproj/theme",
                    value='{"mode":"dark"}',
                    prop="application/json")

# 带 TTL
svc.save_custom_data("tmp_token",
                    value="eyJ...", ttl=3600)

# Raw bytes
svc.save_custom_data("user_avatar_42",
                    raw_body=open("avatar.png","rb").read())
```

### Node.js

```javascript
await svc.saveCustomData('myproj/theme', {
  value: JSON.stringify({ mode: 'dark' }),
  prop: 'application/json',
});

await svc.saveCustomData('tmp_token', { value: tokenStr, ttl: 3600 });
```

---

## 55 · 删除值 `data/{key}` DELETE

### 查询参数

| 字段 | 说明 |
|------|------|
| `isPrefix` | `"true"` 时把 `key` 当前缀，批量清除；否则只删单条 |

### 响应

- 单条（实采）：`{"data":"nb_kv_a_...","status":"ok"}`
- 前缀（实采）：`{"data":"nb_prefix_...","isPrefix":true,"status":"ok"}`

### Python

```python
svc.delete_custom_data("myproj/theme")

# 前缀批量
svc.delete_custom_data("myproj/user42/", is_prefix=True)

# 列表批量（走 ;; 分隔）
svc.delete_custom_data(["a", "b", "c"])
```

---

## 56 · 批量存值 `datas` POST

**URL**：`POST /api/v1/service/datas`

### Body

`[{"key":..,"value":..,"prop":..,"ttl":"60"}]`，注意 `ttl` 是**字符串**秒数。

### 响应（实采）

```json
{ "status": ["ok"] }
```

### Python

```python
svc.save_multiple_custom_data([
    {"key": "cfg/a", "value": "1"},
    {"key": "cfg/b", "value": "2", "ttl": "3600"},
])
```

### Node.js

```javascript
await svc.saveMultipleCustomData([
  { key: 'cfg/a', value: '1' },
  { key: 'cfg/b', value: '2', ttl: '3600' },
]);
```

---

## 57 · 按前缀列键 `datakeys` GET

### 查询参数

| 字段 | 说明 |
|------|------|
| `prefix` | 前缀（自动加 `data_`） |
| `retDataType` | `value` / `prop` / `valueprop` —— 同时取出值，省一次往返 |
| `isSys` | `true` 时返回带 `data_` 内部前缀的系统键（仅 root 场景） |

### 响应

- 仅 keys（实采）：

```json
{ "keys": ["nb_kv_a_1776832185", "nb_kv_b_1776832185"] }
```

- 带值（`retDataType=valueprop`，实采）：

```json
{
  "keys":   ["nb_kv_a_1776832185", "nb_kv_b_1776832185"],
  "props":  ["text/plain",          "bp-1"],
  "values": ["hello-vjmap",         "batch-1"],
  "status": ["ok",                  "ok"]
}
```

### Python

```python
keys = svc.get_custom_data_keys(prefix="myproj/")["keys"]

# 同时取值
bundle = svc.get_custom_data_keys(prefix="myproj/", ret_data_type="valueprop")
for k, v, p in zip(bundle["keys"], bundle["values"], bundle["props"]):
    print(k, v, p)
```

---

## 最佳实践

1. **前缀分层**：用 `/`/`:` 建立命名空间，如 `myproj/user42/tool_state`。这样前缀批量删除、列出都方便。
2. **大文件不要存 KV**：value 单条一般 < 1 MB；大图/DWG 用 `/map/uploadmap` 或独立对象存储。
3. **TTL 清理**：用 `ttl` 让后端帮忙过期；别自己写 crontab。
4. **`keyNoExist` ≠ 错误**：200 正常响应里 `status == "keyNoExist"` 表示没值，而不是失败。

---

---

## api/09-fullsearch.md
<a id=api09-fullsearchmd></a>

# 全文搜索 (Full-text Search)

> 分类代号：C.9  本章共 **3 个接口**  
> **可关闭特性**：`config.yaml` 里 `fullsearch.disable: true` 会让这组路由完全不注册；当 `full_search` 返回 `VjmapError(404)` 时即判定为"关闭"。

vjmap 内置 [bleve](https://github.com/blevesearch/bleve) 作为 CAD 图纸全文索引，主要用于搜"图上某段文字在哪儿"。典型使用场景：

- 用户在前端搜索框里输入"消防栓"，后端返回所有匹配到的文字 + bbox，前端画框高亮
- 按图元类型（单行/多行/属性注记/图层名/块名…）过滤
- 按工作区 / map_ver 隔离

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 58 | `full_search` / `fullSearch` | `POST /api/v1/service/fullsearch/search`（也支持 GET） |
| 59 | `full_search_add` / `fullSearchAdd` | `POST /api/v1/service/fullsearch/add` |
| 60 | `full_search_delete` / `fullSearchDelete` | `DELETE /api/v1/service/fullsearch/delete` |


---

## Document 数据结构

```jsonc
{
  "id":       "unique-id",          // 必填，全库唯一
  "map_ver":  "sys_zp_v1",          // 建议填，便于按图过滤
  "content":  "消防栓",              // 被索引的文本
  "type":     1,                    // 见下方 type 表
  "min_x":    0.0,                  // bbox
  "min_y":    0.0,
  "max_x":    1.0,
  "max_y":    1.0,
  "props":    {...},                // 任意附加属性
  "workspace":"default",            // 为空 = 匿名工作区
  "data":     "..."                  // 原始业务载荷（由你自己定义）
}
```

### `type` 常用值

| type | 说明 |
|-----:|------|
| 1 | 单行文字 |
| 2 | 多行文本 MText |
| 3 | 属性注记 Attribute |
| 4 | 块属性注记 |
| 5 | 图层名 |
| 6 | 块名 |
| 7 | 线型名 |
| 8 | 填充符号名 |
| 9 | 用户自定义 |

---

## 58 · 搜索 `fullsearch/search`

两种调用方式：

- **GET**：所有参数走 query，**适合简单关键词**。
- **POST**：所有参数走 JSON body，**支持复杂结构**（尤其 `bounding_box`）。

### 请求字段

| 字段 | 类型 | 说明 |
|------|------|------|
| `query` | string | 搜索关键词 |
| `map_ver` | string | 多个用 `,` 分隔；纯 mapid 自动取最新版本 |
| `type` | string | 类型过滤：`"1"` / `"1,2,5"` / `"1-3"` / `"1-3,6"` |
| `workspace` | string | 工作区，多个逗号分隔 |
| `limit` | int | 每页条数，默认 10 |
| `offset` | int | 分页偏移 |
| `fields` | string | 返回字段过滤，逗号分隔 |
| `facet` | bool | 是否按 `map_ver` 聚合 facet |
| `bounding_box` | string | `"minx,miny,maxx,maxy"` 或 `"[..]"` |
| `time_range` | string | `"2023-01-01~2023-12-31"` 或 `"7"`（最近 N 天） |

### 响应（实采）

```json
{
  "total": 0,
  "hits": [],
  "took": 2
}
```

`total` 是命中数量，`hits` 是文档数组（含 `id` / `content` / `score` / `fragments` 等 bleve 字段），`took` 是耗时毫秒。

### Python

```python
res = svc.full_search(
    query="消防栓",
    map_ver="proj_v1",
    type_="1,2",
    bounding_box="[0,0,1000,1000]",
    limit=20,
)
for hit in res.get("hits", []):
    print(hit["id"], hit.get("content"))
```

用 GET 走查询串：

```python
svc.full_search(query="消防栓", limit=5, use_get=True)
```

### Node.js

```javascript
const res = await svc.fullSearch('消防栓', {
  mapVer: 'proj_v1',
  type: '1,2',
  boundingBox: '[0,0,1000,1000]',
  limit: 20,
});
```

---

## 59 · 新增文档 `fullsearch/add`

### Body

```json
{
  "document": { "id":"...","map_ver":"...","content":"..." }
}
```

### 响应（实采）

```json
{
  "id": "nb_fs_1776832372",
  "message": "Document added successfully"
}
```

### Python

```python
svc.full_search_add({
    "id": "firehydrant_01",
    "map_ver": "proj_v1",
    "content": "消防栓",
    "type": 1,
    "min_x": 100, "min_y": 200, "max_x": 110, "max_y": 210,
})
```

> **批量导入**：本 SDK 只封装了单条 `full_search_add`。接口 `/fullsearch/batchAdd` 用同样的 POST JSON（`{"documents":[...]}`）；本次不做默认封装，可按需自行 `svc._request("POST", svc.service_url("fullsearch/batchAdd"), json_body={"documents":[...]})`。

---

## 60 · 删除文档 `fullsearch/delete`

注意是 **DELETE 带 JSON body**，而不是 `?id=`（gin 用 `ShouldBindJSON`）。

### Body

```json
{ "id": "firehydrant_01" }
```

### 响应（实采）

```json
{
  "id": "nb_fs_1776832372",
  "message": "Document deleted successfully"
}
```

### Python

```python
svc.full_search_delete("firehydrant_01")
```

---

## 常见错误

| HTTP 状态 | body 片段 | 含义 |
|-----------|-----------|------|
| 400 | `"error":"Invalid request parameters"` | JSON 反序列化失败 |
| 400 | `"error":"Document ID cannot be empty"` | 未填 `id` |
| 400 | `"error":"Invalid bounding box format"` | `bounding_box` 解析失败 |
| 404 | 整条路由不存在 | `fullsearch.disable:true`，功能关闭 |
| 500 | `"error":"Failed to initialize service"` | 后端索引损坏，看服务器日志 |

---

## 使用建议

1. **用 `map_ver` 隔离**：别把所有图纸的文字都塞一个索引里查全局，会拖慢。
2. **先 bbox 后全文**：多数场景只需搜当前视口，给 `bounding_box` 可大幅缩小候选集。
3. **批量构建走后台 job**：把"从 DWG 抽文字 → 批量 add"放到离线任务里；前端只做搜索。
4. **关闭索引也没关系**：SDK 的 `full_search` 会在 404 时抛 `VjmapError(404)`，应用层 catch 一下退化到普通查询即可。

---

---

## api/10-symbols.md
<a id=api10-symbolsmd></a>

# 符号库

> 分类代号：C.10  本章共 **3 个接口（只读）**

vjmap 自带一个"常用图块/图符"库，按分类组织。二开场景里最常见的用法是：

- 前端工具条展示"符号面板"，用户选一个图符，拖到图上
- 拖放后前端本地生成图元并调后端 `updatemap` 持久化
- 导出 DWG 时把符号实例替换为 DXF BlockRef

> 本章只覆盖**只读**三接口（分类列表 / 符号列表 / 符号详情）。写操作（增 / 改 / 删分类与符号）SDK 暂不封装；如果你用 root token，可按 `POST /symbol/category`、`PUT /symbol/:id` 等直接 `svc._request(...)` 调。

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 61 | `symbol_categories` / `symbolCategories` | `GET /api/v1/service/symbol/categories` |
| 62 | `symbol_list` / `symbolList` | `GET /api/v1/service/symbol/list/{categoryId}` |
| 63 | `symbol_get` / `symbolGet` | `GET /api/v1/service/symbol/{id}` |


---

## 61 · 分类列表 `symbol/categories`

### 响应（实采）

```json
{
  "data": [
    { "id": "default",     "name": "默认分类",  "createTime": "2026-04-10 11:29:15" },
    { "id": "user_custom", "name": "用户自定义", "createTime": "2026-04-10 11:29:15" }
  ],
  "status": "ok"
}
```

### Python

```python
cats = svc.symbol_categories()["data"]
for c in cats:
    print(c["id"], c["name"])
```

### Node.js

```javascript
const cats = (await svc.symbolCategories()).data;
```

---

## 62 · 符号列表 `symbol/list/{categoryId}`

按分类分页列出符号。

### 查询参数

| 字段 | 默认 | 说明 |
|------|-----:|------|
| `page` | 1 | 分页起始（1-based） |
| `pageSize` | 20 | 每页 |
| `keyword` | - | 名称模糊匹配 |

### 响应（实采：分类为空）

```json
{
  "data": {
    "list": [],
    "total": 0,
    "page": 1,
    "pageSize": 3
  },
  "status": "ok"
}
```

有数据时 `list` 元素形如：

```jsonc
{
  "id":         "symbol_abc",
  "categoryId": "default",
  "name":       "消防栓",
  "basePoint":  [0, 0],
  "thumbnail":  "data:image/png;base64,...",
  "data":       "...DWG/SVG 原始数据..."
}
```

### Python

```python
page = svc.symbol_list("default", page=1, page_size=20, keyword="栓")
for it in page["data"]["list"]:
    print(it["id"], it["name"])
```

### Node.js

```javascript
const page = await svc.symbolList('default', { page: 1, pageSize: 20 });
```

---

## 63 · 符号详情 `symbol/{id}`

### 响应

- 成功：`{ "status": "ok", "data": { ...同上"有数据时"结构... } }`
- 未找到：HTTP **404** + `{ "status": "error", "message": "not found" }`

### Python

```python
try:
    sym = svc.symbol_get("symbol_abc")["data"]
except vjmap.VjmapError as e:
    if e.status == 404:
        ...  # 符号不存在
    else:
        raise
```

---

## 使用范式

下面是一个"前端面板"级别的整合：

```python
def list_all_symbols():
    for cat in svc.symbol_categories()["data"]:
        page = 1
        while True:
            bundle = svc.symbol_list(cat["id"], page=page, page_size=50)["data"]
            for sym in bundle.get("list", []):
                yield cat, sym
            if page * bundle["pageSize"] >= bundle["total"]:
                break
            page += 1
```

---

---

## api/11-system.md
<a id=api11-systemmd></a>

# 系统

> 分类代号：C.11  本章共 **4 个接口**

这几个接口是运维和监控最常看的：

- **heart**：心跳
- **version**：版本与构建信息
- **runstatus**：当前进程 / 连接 / 切片状态
- **viewlogs**：读最近 N 条日志

生产环境完全可以周期性采集 `runstatus` 做监控告警，**不需要登进后端机器**。

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 64 | `heart` / `heart` | `GET /heart` |
| 65 | `version` / `version` | `GET /version` |
| 66 | `run_status` / `runStatus` | `GET /api/v1/map/cmd/runstatus/_/v1?detail=true` |
| 67 | `view_logs` / `viewLogs` | `GET /api/v1/map/cmd/viewlogs/_/v1?record=N` |


---

## 64 · 心跳 `/heart`

直接挂 Gateway 上（**不走 `/api/v1/...` 前缀**），用来判断进程是否活着。

### 响应（实采）

```json
{
  "status": "ok",
  "timestamp": "2026-04-22T12:37:54.501381+08:00"
}
```

### Python / Node.js

```python
svc.heart()
```

```javascript
await svc.heart();
```

### 用作健康检查

```yaml
# docker-compose healthcheck 片段
healthcheck:
  test: ["CMD", "curl", "-f", "http://127.0.0.1:27660/heart"]
  interval: 30s
  timeout: 3s
  retries: 3
```

---

## 65 · 版本 `/version`

### 响应（实采）

```json
{
  "arch":    "amd64",
  "os":      "windows",
  "status":  "ok",
  "time":    "2026-04-22 12:37:54",
  "version": "2026041001"
}
```

- `version` 是 service 的 build id（yyyyMMDD + 计数）
- `time`  是服务器当前时间，可用来校时
- 图形服务的版本在 `runstatus → application.version` 里（本例是 `"20260305"`）

---

## 66 · 运行状态 `runstatus`

### 查询参数

| 字段 | 默认 | 说明 |
|------|-----:|------|
| `detail` | `false` | `true` 时额外返回 `activeRequests` 列表 |

### 响应（实采 `detail=true`）

```json
{
  "application": {
    "pid":                   "11996",
    "runtime":               24063,
    "taskActiveThreadCount": 0,
    "threadId":              "12000",
    "version":               "20260305"
  },
  "process": {
    "activeProcesss":            {},
    "activeRequestCount":        0,
    "activeRequests":            [],
    "curParallelProcessGeomNum": 0,
    "maxParallelProcessGeomNum": 2,
    "recycleProcesss":           [],
    "reqTimeout":                600000,
    "waitWorkMapIds":            []
  },
  "slice": {},
  "store": {
    "connectionNames":        "_cache_map",
    "storeLastAccessTime":    [],
    "storeNoActiveCloseTime": 60,
    "storeStatus":            []
  }
}
```

### 字段速查

| 字段 | 含义 |
|------|------|
| `application.runtime` | 进程启动毫秒数 |
| `process.activeRequestCount` | 当前在处理的请求数 |
| `process.activeRequests` | `detail=true` 时的 URL 列表，排查"卡在什么请求上" |
| `process.curParallelProcessGeomNum` | 当前并行几何任务 |
| `process.maxParallelProcessGeomNum` | 并发上限 |
| `process.recycleProcesss` | 被回收的子进程 |
| `slice` | 切片缓存状态（按 layer） |
| `store` | KV 存储连接 |

### Python

```python
status = svc.run_status(detail=True)
assert status["application"]["pid"]
print(status["process"]["activeRequestCount"], "个并发请求")
```

### Node.js

```javascript
const st = await svc.runStatus(true);
```

---

## 67 · 查看日志 `viewlogs`

### 查询参数

| 字段 | 默认 | 说明 |
|------|-----:|------|
| `record` | 100 | 读取最近多少条记录 |

### 响应（实采，部分片段）

```json
{
  "logs": [
    "       *** log260422_0x115f4_2026-04-22.log ***  ",
    "[2026-04-22 11:58:01.625] [daily_logger] [info] File:() Line:(0) Msg: (\"process app started..._null_@>v1_1571__||__11996__||__28011__||__D:/Program Files/vjmapserver/data\")\n",
    "[2026-04-22 11:58:01.697] [daily_logger] [info] File:() Line:(0) Msg: (openMap begin... \"_null_@>v1\")\n",
    ...
  ]
}
```

每一条都是一行日志（带行尾 `\n`）；多子进程时会用 `*** log....log ***` 分隔不同文件。

### Python

```python
lines = svc.view_logs(record=200)["logs"]
for line in lines[-20:]:
    print(line.rstrip())
```

### Node.js

```javascript
const { logs } = await svc.viewLogs(200);
```

> ⚠️ 这是**最近 N 条**，不是整个日志文件；想做审计请定时拉并本地存档。

---

## 监控样例：Python 拉取 → Prometheus

```python
from time import sleep
def poll():
    while True:
        try:
            s = svc.run_status(detail=False)
            metrics = {
                "vjmap_up":             1,
                "vjmap_active_reqs":    s["process"]["activeRequestCount"],
                "vjmap_runtime_ms":     s["application"]["runtime"],
                "vjmap_store_count":    len(s["store"].get("storeStatus") or []),
            }
        except Exception:
            metrics = {"vjmap_up": 0}
        push_to_prometheus(metrics)
        sleep(15)
```

---

---

## api/12-service-misc.md
<a id=api12-service-miscmd></a>

# /service 杂项

> 分类代号：C.12  本章共 **3 个接口**

这里汇集了"不方便归到其它分类、但二开很常用"的三个能力：

- **QR Code 生成**：把字符串直接渲染成 PNG 二维码
- **字体字形 (PBF)**：vjmap地图渲染依赖的字体文件
- **图片上传**：把任意图片文件塞到服务器指定路径（配合前端水印/图床）

---

## 快速索引

| 序号 | SDK 方法 | URL |
|----:|---------|-----|
| 68 | `qrcode_url` / `qrcode_bytes` / `qrcodeUrl` / `qrcodeBytes` | `GET /api/v1/service/qrcode?content=...&size=` |
| 69 | `glyphs_url` / `glyphs_bytes` / `glyphsUrl` / `glyphsBytes` | `GET /api/v1/service/fonts/{fontstack}/{range}.pbf` |
| 70 | `upload_img` / `uploadImg` | `POST /api/v1/service/uploadImg` (multipart) |


---

## 68 · 二维码 `qrcode`

### 查询参数

| 字段 | 默认 | 说明 |
|------|-----:|------|
| `content` | -（必填） | 要编码的字符串；后端会 `decodeURIComponent`，所以**不用双重编码** |
| `size` | 256 | 图片边长（像素） |

### 响应

直接返回 `image/png` 字节，**不是 JSON**。

### SDK 两种用法

1. **URL 构造**（适合前端直接 `<img src=...>`）：

```python
url = svc.qrcode_url("https://example.com/project/42", size=512)
# http://127.0.0.1:27660/api/v1/service/qrcode?content=https%3A%2F%2Fexample.com%2Fproject%2F42&size=512&token=<jwt>
```

2. **下载字节**（适合服务端生成后转发/保存）：

```python
png = svc.qrcode_bytes("VJMAP", size=128)
open("qr.png", "wb").write(png)
```

Node.js：

```javascript
const url = svc.qrcodeUrl('VJMAP', 512);
const buf = await svc.qrcodeBytes('VJMAP', 128);
```

### 实采信息

```json
// qrcode_bytes_info.json
{ "len": 284, "md5": "2577b1157cf93feefce8c6b180a0ff42", "magic_ok": true }
```

---

## 69 · 字体 `fonts/{fontstack}/{range}.pbf`

vjmap 矢量渲染在画文字时，需要按需下载对应字体、对应 Unicode 区间的 PBF 字形文件。这个接口就是字形的分发端。

### 路径参数

| 字段 | 说明 |
|------|------|
| `fontstack` | 字体名，可用 `,` 分隔多个**回退栈**。后端会依次尝试，并**自动在末尾追加 `Arial Unicode MS Regular`** 作为兜底 |
| `range` | Unicode 字形区间，形如 `0-255`、`256-511`。通常是 256 个字形一包 |

### 响应

`application/x-protobuf` 字节（PBF）。

### SDK

```python
url = svc.glyphs_url("Arial Unicode MS Regular", "0-255")
pbf = svc.glyphs_bytes("Arial Unicode MS Regular", "0-255")
```

```javascript
const url = svc.glyphsUrl('Arial Unicode MS Regular', '0-255');
```

### 实采

```json
// glyphs_bytes_info.json
{
  "len": 75282,
  "md5": "638faf3bf9a90424d23513cfab7e9ce1"
}
```

### 用在 vjmap 里

```js
map.setStyle({
  glyphs: 'http://127.0.0.1:27660/api/v1/service/fonts/{fontstack}/{range}.pbf?token=' + TOKEN,
  // ...其它
});
```

注意 `{fontstack}`/`{range}` 是 **vjmap 自己替换**的占位符，别让 URL 编码器把 `{` / `}` percent-encode 掉。

---

## 70 · 上传图片 `uploadImg`

### 特殊鉴权

这个接口**不走**普通 token，而是检查 HTTP header：

```
Authorization: Basic X3ZqbWFwQF8=
```

即 Basic base64("`_vjmap@_`")。后端硬编码。SDK 默认已经带上；你也能传自定义 `authorization` 参数覆盖。

### Headers

| Header | 说明 |
|--------|------|
| `Authorization` | 上述固定值（SDK 默认） |
| `Uri` | **必填**：服务器端保存目录（以 `/` 结尾）。SDK 参数名 `uri` |
| `watermark` | `"1"` 时后端自动叠加水印（`utils.NewWatermarkBase64` 实现） |

### multipart 字段

| 字段 | 说明 |
|------|------|
| `upload` | 文件内容。**注意 field name 是 `upload` 不是 `file`** |

### 响应

成功（实采）：

```json
{
  "code": 0,
  "path": "./data/test_upload/nb_upload_smoke.png",
  "url":  "nb_upload_smoke.png"
}
```

失败（实采：错误 Authorization）：

```json
{
  "code": 7,
  "data": {},
  "msg": "no Authorization"
}
```

### Python

```python
# 从本地文件上传
svc.upload_img(
    "./avatar.png",
    uri="./data/upload/avatars/",
    filename="user_42.png",
    watermark=True,
)

# 从字节上传
svc.upload_img(
    ("user_42.png", png_bytes),
    uri="./data/upload/avatars/",
)
```

### Node.js

```javascript
// Node 18+
await svc.uploadImg('./avatar.png', {
  uri: './data/upload/avatars/',
  filename: 'user_42.png',
  watermark: true,
});

// Buffer
await svc.uploadImg(buf, {
  uri: './data/upload/avatars/',
  filename: 'user_42.png',
});
```

---

## api/13-ai.md
<a id=api13-aimd></a>

# 13. AI（向量检索 / FAQ / 文档）

> **条件启用**：这一组接口依赖 `vjmap` 服务端的嵌入（embedding）模型与向量库。
> 若未正确配置，调用会返回 `500`（例如缺少 `bge-large-zh-v1.5-f16.gguf` 模型文件）。
> 在你的环境若未开启 AI 子服务，直接跳过本章。

本章覆盖 3 个接口：

| No. | 接口 | URL | Method | 说明 |
|-----|------|-----|--------|------|
| 13.1 | vectorSearch | `/api/v1/service/vectorSearch` | GET / POST | 相似性检索 |
| 13.2 | chatFaq | `/api/v1/service/chatFaq` | GET | 获取 FAQ 知识库 |
| 13.3 | vectorDocs | `/api/v1/service/vectorDocs` | GET | 获取向量库已有文档元数据 |

---

## 13.0 启用探针

SDK 提供 `aiEnabled()` / `ai_enabled()`（内部调用 `chatFaq`）判断 AI 子服务是否可用；
失败（抛 `VjmapError`）则视为未启用，跳过后续调用即可。

```python
if not svc.ai_enabled():
    raise SystemExit("AI 未启用，略")
```

---

## 13.1 vectorSearch — 相似性检索

按给定自然语言 `query` 在向量库中检索最相似的若干条文档。

### 请求

两种请求方式：

| 方式 | URL | Body | 备注 |
|------|-----|------|------|
| GET | `GET /api/v1/service/vectorSearch?query=...&numDocuments=...` | 无 | 简单场景用 |
| POST（推荐） | `POST /api/v1/service/vectorSearch` | JSON | 支持 `documents` 等复杂字段 |

### 主要参数

| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `query` | string | — | **必填** 查询文本 |
| `numDocuments` | int | 20 | 返回条数 |
| `collection` | string | `"vj"` | 向量库集合名 |
| `where` | string | — | metadata 过滤表达式（[chromem-go](https://github.com/philippgille/chromem-go) 语法） |
| `whereDocument` | string | — | document 内容过滤表达式 |
| `searchDescription` | string | — | 附加搜索上下文，帮助模型更精准 |
| `disableSplitSentence` | bool | false | 关闭"先分句再分别 embed" |
| `disableSearchKnowledge` | bool | false | 仅搜 `documents`，不搜知识库 |
| `documents` | array | — | 临时附带的用户文档（不会持久化）。每项：`{content:"...", metadata:{...}}` |

### 响应

```json
{
  "status": "ok",
  "data": [
    {
      "id": "__createPolygon",
      "content": "根据给定的点坐标数组直接绘制一个多边形",
      "metadata": { "...": "..." },
      "similarity_score": 0.8273
    },
    { "...": "..." }
  ]
}
```

### SDK 示例

```python
res = svc.vector_search("如何画多边形", num_documents=5)
for doc in res.get("data", []):
    print(doc["id"], doc.get("similarity_score"))
```

```js
const res = await svc.vectorSearch('如何画多边形', { numDocuments: 5 });
res.data.forEach(d => console.log(d.id, d.similarity_score));
```

### 本地实测（失败样例）

当前测试环境缺少 embedding 模型文件，真实请求返回：

```json
{
  "status": 500,
  "body": {
    "code": 8,
    "msg": "D:/Program Files/vjmapserver/data/ai/models/bge-large-zh-v1.5-f16.gguf is not exist"
  }
}
```

> **部署提示**：部署方需根据官方文档下载 embedding 模型文件，放置于
> 服务端的 `data/ai/models/` 目录下，并在配置中指定模型名。未配置时，
> `vectorSearch` 会失败但不影响其他子系统运行。

---

## 13.2 chatFaq — FAQ 知识库

### 请求

```
GET /api/v1/service/chatFaq
```

### 响应

返回一个按"主题"分组的字符串数组字典。

```json
{
  "status": "ok",
  "data": {
    "UI交互": [
      "弹出学生表单输入对话框，包括姓名、年龄(默认18)、性别",
      "获取地图的当前中心点坐标X和Y、地图级别，生成json对象，根据这数据弹出数据信息框..."
    ],
    "信息窗口": [
      "在坐标[30,50]处添加一个信息窗口，内容为\"Hello,World\""
    ],
    "几何算法": [ "..." ]
  }
}
```

数据源来自服务端 `./data/ai/faq/*.md`，并开启 5 分钟 HTTP 缓存。

### SDK 示例

```python
faq = svc.chat_faq()
for topic, items in faq["data"].items():
    print(f"- {topic}: {len(items)} 项")
```

```js
const { data } = await svc.chatFaq();
for (const [topic, items] of Object.entries(data)) {
  console.log(`- ${topic}: ${items.length} 项`);
}
```

---

## 13.3 vectorDocs — 向量库文档

### 请求

```
GET /api/v1/service/vectorDocs
```

### 响应

返回一个数组，每项是知识库里的一条文档元数据：

```json
{
  "status": "ok",
  "data": [
    {
      "id": "__createPolygon",
      "content": "根据给定的点坐标数组直接绘制一个多边形",
      "metadata": {
        "category": "draw",
        "code": "...",
        "comments": "...",
        "function": "__createPolygon",
        "param": "..."
      },
      "similarity_score": 0
    }
  ]
}
```

`similarity_score` 恒为 0（此接口不做相似度计算，仅导出文档）。

### SDK 示例

```python
docs = svc.vector_docs()["data"]
print("知识库条目数：", len(docs))
print("分类统计：", {
    c: sum(1 for d in docs if d["metadata"].get("category") == c)
    for c in sorted({d["metadata"].get("category", "") for d in docs})
})
```

```js
const { data: docs } = await svc.vectorDocs();
console.log('条目数：', docs.length);
```

---

## 使用建议

1. **部署前先跑探针**：初始化客户端后先调 `chat_faq()`，失败则关闭 AI UI 入口。
2. **前端搜索**：优先用 POST `vectorSearch`，把 `searchDescription` 带上业务上下文能显著提升命中率。
3. **冷启动**：第一次调用 `vectorSearch` 会加载 embedding 模型，响应可能秒级；
   第二次起基本在毫秒级。
4. **安全**：`vectorDocs` 直接返回所有文档内容（含 `code` 字段），不要把它暴露给未鉴权用户。

---
