viatris-exam-cli 1.6.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -13
- package/package.json +2 -2
- package/skills/viatris-exam-cli/SKILL.md +15 -9
- package/skills/viatris-exam-cli/references/cli-reference.md +86 -48
- package/skills/viatris-exam-cli.zip +0 -0
- package/src/app.js +14 -2
- package/src/cli.js +0 -0
- package/src/command-handlers/knowledge.js +19 -3
- package/src/command-handlers/products.js +2 -1
- package/src/config.js +17 -7
- package/src/credentials.js +100 -0
- package/src/help.js +9 -4
- package/src/metadata.js +17 -4
- package/src/sanitizers/documents.js +113 -0
package/README.md
CHANGED
|
@@ -5,8 +5,17 @@
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm
|
|
8
|
+
npm install -g viatris-exam-cli
|
|
9
9
|
export VIATRIS_API_KEY="replace-with-real-key"
|
|
10
|
+
viatris-exam login
|
|
11
|
+
unset VIATRIS_API_KEY
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
开始任务前更新到最新版本:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g viatris-exam-cli@latest
|
|
18
|
+
viatris-exam --version
|
|
10
19
|
```
|
|
11
20
|
|
|
12
21
|
默认连接线上环境:
|
|
@@ -29,25 +38,27 @@ https://apaaspsc-app_17gbe211qq5-1879207268486311.aiforce.run/app/app_17gbe211qq
|
|
|
29
38
|
|
|
30
39
|
`VIATRIS_API_ENV` 支持 `production`、`development`,以及别名 `prod`、`dev`。如需临时连接其他部署,可设置 `VIATRIS_API_BASE_URL`;该变量的优先级高于环境预设。
|
|
31
40
|
|
|
32
|
-
CLI 不接受 `--api-key`
|
|
41
|
+
`login` 将 Key 写入 `~/.viatris-exam/.credentials.json`。隐藏目录权限为 `0700`,凭证文件权限为 `0600`。CLI 不接受 `--api-key` 参数,也不会输出 Key。
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
X-API-Key: <VIATRIS_API_KEY>
|
|
36
|
-
```
|
|
43
|
+
读取顺序:
|
|
37
44
|
|
|
38
|
-
|
|
45
|
+
1. 优先读取隐藏凭证文件。
|
|
46
|
+
2. 文件不存在时读取 `VIATRIS_API_KEY`。
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
默认认证格式:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Authorization: Bearer <API key>
|
|
43
52
|
```
|
|
44
53
|
|
|
54
|
+
无需设置 `VIATRIS_API_KEY_HEADER` 或 `VIATRIS_API_KEY_PREFIX`。
|
|
55
|
+
|
|
45
56
|
## Agent 接口约定
|
|
46
57
|
|
|
47
58
|
- API 成功响应以 JSON 写入 `stdout`。
|
|
48
59
|
- 错误以单行结构化 JSON 写入 `stderr`。
|
|
49
60
|
- POST/PUT 请求从文件、`stdin` 或 `--json` 读取 JSON 对象。
|
|
50
|
-
- API Key
|
|
61
|
+
- API Key 优先从隐藏凭证文件读取,环境变量作为兜底。
|
|
51
62
|
- 默认使用线上环境,可通过 `VIATRIS_API_ENV=development` 切换到开发环境。
|
|
52
63
|
- `VIATRIS_API_BASE_URL` 可覆盖当前环境地址。
|
|
53
64
|
- GET 请求遇到网络错误、HTTP 429 或 5xx 时自动重试,POST/PUT/PATCH/DELETE 不自动重试。
|
|
@@ -114,6 +125,8 @@ viatris-exam knowledge documents delete <document-id>
|
|
|
114
125
|
|
|
115
126
|
`/knowledge` CRUD 响应中的 `level` 是物理存储层级(产品根为 1、固定维度为 2);产品生成接口使用业务层级(产品隐藏、固定维度为 L1、动态节点为 L2-L4)。
|
|
116
127
|
|
|
128
|
+
所有文档写入都会在请求发出前从 `title`、`url`、`summary` 中移除“小荷”“小荷医疗”和“小荷 AI 医生”及其空格变体。当前写接口只接受 `title` 和 `summary`,输入中的 `url` 会在净化后从出站载荷移除,但 URL 净化能力作为永久兼容规则保留。该规则覆盖文档创建、批量 upsert、单文档更新和整树回写中的嵌套文档;标题清洗后为空会直接返回校验错误。
|
|
129
|
+
|
|
117
130
|
### 出题任务
|
|
118
131
|
|
|
119
132
|
```bash
|
|
@@ -196,14 +209,14 @@ printf '%s' '{"query":"阿托伐他汀 适应症","limit":20}' |
|
|
|
196
209
|
|
|
197
210
|
```text
|
|
198
211
|
--base-url <url> 覆盖当前环境地址
|
|
199
|
-
--key-header <name>
|
|
200
|
-
--key-prefix <value>
|
|
212
|
+
--key-header <name> 覆盖认证头,默认 Authorization
|
|
213
|
+
--key-prefix <value> 覆盖认证前缀,默认 "Bearer "
|
|
201
214
|
--timeout <ms> 超时时间,默认 30000
|
|
202
215
|
--retries <n> GET 重试次数,默认 2
|
|
203
216
|
--compact 输出单行紧凑 JSON
|
|
204
217
|
```
|
|
205
218
|
|
|
206
|
-
地址选择优先级为 `--base-url`、`VIATRIS_API_BASE_URL`、`VIATRIS_API_ENV` 对应的预设地址。认证 Key
|
|
219
|
+
地址选择优先级为 `--base-url`、`VIATRIS_API_BASE_URL`、`VIATRIS_API_ENV` 对应的预设地址。认证 Key 优先使用隐藏凭证文件,文件不存在时使用 `VIATRIS_API_KEY`。
|
|
207
220
|
|
|
208
221
|
## 项目结构
|
|
209
222
|
|
|
@@ -217,7 +230,10 @@ src/
|
|
|
217
230
|
├── args.js # 参数解析与校验
|
|
218
231
|
├── input.js # JSON 文件、stdin 与内联输入
|
|
219
232
|
├── config.js # 环境、地址与认证配置
|
|
233
|
+
├── credentials.js # 隐藏凭证文件读写
|
|
220
234
|
├── http.js # HTTP、重试与响应处理
|
|
235
|
+
├── sanitizers/
|
|
236
|
+
│ └── documents.js # 文档永久净化规则
|
|
221
237
|
├── metadata.js # 版本、命令和环境元数据
|
|
222
238
|
├── help.js # 帮助文本
|
|
223
239
|
├── output.js # 标准 JSON 输出
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viatris-exam-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Agent-friendly CLI for the Viatris exam OpenAPI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
14
|
"test": "node --test",
|
|
15
|
-
"check": "node --check src/*.js && node --check src/command-handlers/*.js",
|
|
15
|
+
"check": "node --check src/*.js && node --check src/command-handlers/*.js && node --check src/sanitizers/*.js",
|
|
16
16
|
"check:versions": "node scripts/check-versions.js",
|
|
17
17
|
"check:skill": "node scripts/check-skill.js",
|
|
18
18
|
"pack:skill": "cd skills && /bin/rm -f viatris-exam-cli.zip && /usr/bin/zip -qr viatris-exam-cli.zip viatris-exam-cli -x '*.DS_Store'"
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: viatris-exam-cli
|
|
3
|
-
description: Operate the Viatris exam OpenAPI through its agent-friendly CLI. Use for products, knowledge
|
|
4
|
-
version: 1.
|
|
3
|
+
description: Operate the Viatris exam OpenAPI through its agent-friendly CLI. Use for login, products, knowledge, generation tasks, exam records, scoring, exams, questions, or medical search.
|
|
4
|
+
version: 1.8.1
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Viatris Exam CLI
|
|
8
8
|
|
|
9
|
-
Use the
|
|
9
|
+
Use the installed CLI for every supported API operation. Do not call the HTTP endpoints directly when the CLI covers the task.
|
|
10
10
|
|
|
11
11
|
## Start
|
|
12
12
|
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
3.
|
|
16
|
-
4.
|
|
17
|
-
5.
|
|
13
|
+
1. Read the install and update sections in [references/cli-reference.md](references/cli-reference.md).
|
|
14
|
+
2. Install `viatris-exam-cli` from npm when unavailable.
|
|
15
|
+
3. Update to `viatris-exam-cli@latest` at the start of a task so capability instructions match the installed CLI.
|
|
16
|
+
4. Verify with `viatris-exam --version`.
|
|
17
|
+
5. Prefer the saved credential. To initialize or rotate it, set `VIATRIS_API_KEY` and run `viatris-exam login`; never place the key in command arguments, output, or logs.
|
|
18
|
+
6. Use production by default. Set `VIATRIS_API_ENV=development` only when the user requests development or testing.
|
|
19
|
+
7. Run `viatris-exam describe --compact` when capability discovery or exact machine-readable metadata is needed.
|
|
20
|
+
8. Read the relevant command section in the reference before constructing a write payload or handling a nonzero exit code.
|
|
18
21
|
|
|
19
22
|
## Execute
|
|
20
23
|
|
|
21
|
-
- Invoke the
|
|
24
|
+
- Invoke commands through the installed `viatris-exam` binary.
|
|
22
25
|
- Prefer `--input <file>` for existing payloads and stdin for generated payloads.
|
|
23
26
|
- Parse successful `stdout` as JSON.
|
|
24
27
|
- Parse failed `stderr` as JSON and branch on both exit code and `error.type`.
|
|
28
|
+
- Authentication defaults to `Authorization: Bearer <key>`. Read the hidden credential file first and use `VIATRIS_API_KEY` only when no saved credential exists.
|
|
25
29
|
- Use `products list` to resolve product master IDs and stable codes before product-specific workflows. `products create` requires only a Chinese name and also initializes the canonical knowledge-tree root.
|
|
26
30
|
- Use `products update` only for verified optional medical master data. Do not infer English names, generic names, indications, dosage forms, or remarks without evidence.
|
|
27
31
|
- Preserve `externalRef` when retrying question submission. It is the idempotency key.
|
|
@@ -29,6 +33,8 @@ Use the repository-local CLI for every supported API operation. Do not call the
|
|
|
29
33
|
- Apply every returned generation rule. Product-scoped rules identify applicable product codes through `products`; global rules return an empty `products` array.
|
|
30
34
|
- Prefer scoring rule V2 for generated questions and scoring results. Use stable point and deduction-rule IDs.
|
|
31
35
|
- Treat product master records as outside business knowledge-tree levels. Fixed dimensions are business L1; generated nodes are L2-L4. Product detail lookup also accepts a historical knowledge-tree root ID.
|
|
36
|
+
- Document writes automatically remove 小荷 source-brand labels, including 小荷医疗 and whitespace variants of 小荷 AI 医生, from title, URL, and summary. Never reintroduce these labels after CLI sanitization.
|
|
37
|
+
- Current document write APIs accept only `title` and `summary`; the CLI drops an input `url` before sending. Keep URL sanitization in the permanent sanitizer for backward and future compatibility.
|
|
32
38
|
- Treat knowledge-tree and recursive node deletion as destructive. Inspect the target first and perform them only when the user explicitly requests deletion.
|
|
33
39
|
- Do not automatically retry POST, PUT, PATCH, or DELETE commands. Inspect the returned error and determine whether the server accepted the request.
|
|
34
40
|
- Keep temporary payloads outside the repository unless they are requested deliverables.
|
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
# CLI Reference
|
|
2
2
|
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
Install the CLI globally so every example below can use the `viatris-exam` command directly:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g viatris-exam-cli
|
|
9
|
+
viatris-exam --version
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Update
|
|
13
|
+
|
|
14
|
+
Update before starting a task so the installed capabilities and this Skill remain aligned:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g viatris-exam-cli@latest
|
|
18
|
+
viatris-exam --version
|
|
19
|
+
```
|
|
20
|
+
|
|
3
21
|
## Authentication And Environment
|
|
4
22
|
|
|
5
|
-
|
|
23
|
+
Login once:
|
|
6
24
|
|
|
7
25
|
```bash
|
|
8
26
|
export VIATRIS_API_KEY="..."
|
|
27
|
+
viatris-exam login
|
|
28
|
+
unset VIATRIS_API_KEY
|
|
9
29
|
```
|
|
10
30
|
|
|
31
|
+
Login writes `~/.viatris-exam/.credentials.json` with file mode `0600`; its containing directory uses mode `0700`. The command never prints the key. Run login again with a new environment value to rotate the saved key.
|
|
32
|
+
|
|
33
|
+
API key lookup order:
|
|
34
|
+
|
|
35
|
+
1. Hidden credential file.
|
|
36
|
+
2. `VIATRIS_API_KEY` environment variable when the file does not exist.
|
|
37
|
+
|
|
11
38
|
Environment selection:
|
|
12
39
|
|
|
13
40
|
| Value | Target |
|
|
@@ -17,25 +44,25 @@ Environment selection:
|
|
|
17
44
|
|
|
18
45
|
`VIATRIS_API_BASE_URL` overrides the selected environment. Use it only for an explicitly supplied custom deployment or local mock.
|
|
19
46
|
|
|
20
|
-
Default authentication
|
|
47
|
+
Default authentication:
|
|
21
48
|
|
|
22
49
|
```text
|
|
23
|
-
|
|
50
|
+
Authorization: Bearer <API key>
|
|
24
51
|
```
|
|
25
52
|
|
|
26
|
-
|
|
53
|
+
No header or prefix environment variables are required. Advanced overrides remain available when connecting to a different compatible service:
|
|
27
54
|
|
|
28
55
|
```bash
|
|
29
|
-
export VIATRIS_API_KEY_HEADER="
|
|
30
|
-
export VIATRIS_API_KEY_PREFIX="
|
|
56
|
+
export VIATRIS_API_KEY_HEADER="Custom-Header"
|
|
57
|
+
export VIATRIS_API_KEY_PREFIX=""
|
|
31
58
|
```
|
|
32
59
|
|
|
33
60
|
## Capability Discovery
|
|
34
61
|
|
|
35
62
|
```bash
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
viatris-exam describe --compact
|
|
64
|
+
viatris-exam --help
|
|
65
|
+
viatris-exam --version
|
|
39
66
|
```
|
|
40
67
|
|
|
41
68
|
`describe` does not require credentials. It returns commands, paths, environment metadata, authentication metadata, and exit codes as JSON.
|
|
@@ -45,7 +72,7 @@ node src/cli.js --version
|
|
|
45
72
|
List product master data:
|
|
46
73
|
|
|
47
74
|
```bash
|
|
48
|
-
|
|
75
|
+
viatris-exam products list --compact
|
|
49
76
|
```
|
|
50
77
|
|
|
51
78
|
The response is `{ "items": [...] }`, ordered by `sortOrder` and then name. Each record includes the stable `code`, Chinese name, optional English name, generic name, indication, dosage form, `active` or `inactive` status, remark, and timestamps.
|
|
@@ -53,7 +80,7 @@ The response is `{ "items": [...] }`, ordered by `sortOrder` and then name. Each
|
|
|
53
80
|
Create a product and initialize its canonical knowledge-tree root:
|
|
54
81
|
|
|
55
82
|
```bash
|
|
56
|
-
|
|
83
|
+
viatris-exam products create --json '{
|
|
57
84
|
"name": "络活喜",
|
|
58
85
|
"genericName": "苯磺酸氨氯地平片",
|
|
59
86
|
"indication": "高血压、冠心病",
|
|
@@ -68,7 +95,7 @@ Only `name` is required and the Chinese name must be unique. `code` is optional;
|
|
|
68
95
|
Get a product and its fixed L1 dimensions:
|
|
69
96
|
|
|
70
97
|
```bash
|
|
71
|
-
|
|
98
|
+
viatris-exam products get "$PRODUCT_ID" --compact
|
|
72
99
|
```
|
|
73
100
|
|
|
74
101
|
Use the product master `id`; historical knowledge-tree root IDs remain accepted for compatibility. The response also contains nullable `nameEn`, `genericName`, `indication`, `dosageForm`, and `remark`. Product master data is outside business levels L1-L4. Use each returned `fixedDimensions[].clientRef` as the parent reference for an L2 node. Each fixed dimension also returns `level: 1` and a display `codePrefix`.
|
|
@@ -76,7 +103,7 @@ Use the product master `id`; historical knowledge-tree root IDs remain accepted
|
|
|
76
103
|
Update verified optional medical master data:
|
|
77
104
|
|
|
78
105
|
```bash
|
|
79
|
-
|
|
106
|
+
viatris-exam products update "$PRODUCT_ID" --json '{
|
|
80
107
|
"nameEn": "Lipitor",
|
|
81
108
|
"genericName": "阿托伐他汀钙片",
|
|
82
109
|
"indication": "高胆固醇血症、混合型血脂异常及心血管疾病预防",
|
|
@@ -90,7 +117,7 @@ Only supplied fields are updated. Each supplied value must be a nonempty string
|
|
|
90
117
|
Replace a product knowledge tree:
|
|
91
118
|
|
|
92
119
|
```bash
|
|
93
|
-
|
|
120
|
+
viatris-exam products update-knowledge-tree "$PRODUCT_ID" \
|
|
94
121
|
--input knowledge-tree.json \
|
|
95
122
|
--compact
|
|
96
123
|
```
|
|
@@ -126,7 +153,6 @@ Payload shape:
|
|
|
126
153
|
"documents": [
|
|
127
154
|
{
|
|
128
155
|
"title": "药品说明书",
|
|
129
|
-
"url": "https://example.com/source",
|
|
130
156
|
"summary": "与该知识点直接对应的证据摘录"
|
|
131
157
|
}
|
|
132
158
|
]
|
|
@@ -140,7 +166,7 @@ Submit the complete L2/L3/L4 node set, between 15 and 500 nodes. Every `clientRe
|
|
|
140
166
|
Delete all dynamic knowledge-tree content:
|
|
141
167
|
|
|
142
168
|
```bash
|
|
143
|
-
|
|
169
|
+
viatris-exam products delete-knowledge-tree "$PRODUCT_ID"
|
|
144
170
|
```
|
|
145
171
|
|
|
146
172
|
This destructive operation preserves the product root and five fixed L1 dimensions, deletes all L2-L4 nodes and evidence documents, and unbinds affected questions. Inspect the product and tree first, and execute only when the user explicitly requests the full reset.
|
|
@@ -150,7 +176,7 @@ This destructive operation preserves the product root and five fixed L1 dimensio
|
|
|
150
176
|
Get the complete physical tree and all product documents:
|
|
151
177
|
|
|
152
178
|
```bash
|
|
153
|
-
|
|
179
|
+
viatris-exam knowledge tree lipitor --compact
|
|
154
180
|
```
|
|
155
181
|
|
|
156
182
|
The incremental `/knowledge` responses expose physical storage levels where the product root is 1 and fixed dimensions are 2. This differs from the product-generation business taxonomy, where the product is hidden, fixed dimensions are L1, and generated content is L2-L4.
|
|
@@ -158,7 +184,7 @@ The incremental `/knowledge` responses expose physical storage levels where the
|
|
|
158
184
|
Create or reuse a visible path:
|
|
159
185
|
|
|
160
186
|
```bash
|
|
161
|
-
|
|
187
|
+
viatris-exam knowledge nodes by-path --json \
|
|
162
188
|
'{"product":"lipitor","path":["疾病知识","血脂异常基础","ASCVD 风险评估"]}'
|
|
163
189
|
```
|
|
164
190
|
|
|
@@ -167,20 +193,20 @@ The path has one to three entries. The first entry must be a fixed dimension nam
|
|
|
167
193
|
Create or reuse one node under a known parent:
|
|
168
194
|
|
|
169
195
|
```bash
|
|
170
|
-
|
|
196
|
+
viatris-exam knowledge nodes create --json \
|
|
171
197
|
'{"parentId":"PARENT_ID","name":"ASCVD 风险评估","sortOrder":20}'
|
|
172
198
|
```
|
|
173
199
|
|
|
174
200
|
Read, update, move, or delete a node:
|
|
175
201
|
|
|
176
202
|
```bash
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
viatris-exam knowledge nodes get "$NODE_ID"
|
|
204
|
+
viatris-exam knowledge nodes update "$NODE_ID" \
|
|
179
205
|
--json '{"name":"血脂异常与他汀治疗","sortOrder":5}'
|
|
180
|
-
|
|
206
|
+
viatris-exam knowledge nodes move "$NODE_ID" \
|
|
181
207
|
--json '{"newParentId":"NEW_PARENT_ID","sortOrder":30}'
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
viatris-exam knowledge nodes delete "$NODE_ID"
|
|
209
|
+
viatris-exam knowledge nodes delete "$NODE_ID" --recursive
|
|
184
210
|
```
|
|
185
211
|
|
|
186
212
|
Product roots and fixed dimensions are protected from rename, move, and deletion. Moving cannot cross products, target the node itself or its descendants, or produce levels deeper than L4. Plain deletion is the safe default and returns HTTP 409 when children or question bindings exist. `--recursive` deletes the full subtree and documents and unbinds affected questions. Inspect the node first and use recursive deletion only when explicitly requested.
|
|
@@ -188,22 +214,34 @@ Product roots and fixed dimensions are protected from rename, move, and deletion
|
|
|
188
214
|
Manage documents:
|
|
189
215
|
|
|
190
216
|
```bash
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
--json '{"title":"说明书","
|
|
194
|
-
|
|
217
|
+
viatris-exam knowledge documents list "$NODE_ID"
|
|
218
|
+
viatris-exam knowledge documents create "$NODE_ID" \
|
|
219
|
+
--json '{"title":"说明书","summary":"摘录"}'
|
|
220
|
+
viatris-exam knowledge documents upsert "$NODE_ID" \
|
|
195
221
|
--json '{"items":[{"title":"说明书","summary":"更新后的摘录"}]}'
|
|
196
|
-
|
|
197
|
-
--json '{"
|
|
198
|
-
|
|
222
|
+
viatris-exam knowledge documents update "$DOCUMENT_ID" \
|
|
223
|
+
--json '{"summary":"更新后的摘要"}'
|
|
224
|
+
viatris-exam knowledge documents delete "$DOCUMENT_ID"
|
|
199
225
|
```
|
|
200
226
|
|
|
201
|
-
Create reuses a document with the same title under the node. Batch upsert accepts 1-20 items and updates `
|
|
227
|
+
Create reuses a document with the same title under the node. Batch upsert accepts 1-20 items and updates `summary` for existing titles. A single-document update changes only supplied fields; send `summary: null` to clear the summary. Current write contracts accept only `title` and `summary`; if input contains `url`, the CLI drops it before sending. Read responses may still contain historical URL data. Inspect a document before an explicitly requested delete.
|
|
228
|
+
|
|
229
|
+
### Document Sanitization
|
|
230
|
+
|
|
231
|
+
Before every document write, the CLI removes source-brand labels from `title`, `url`, and `summary`. This applies to document create, batch upsert, single-document update, and documents nested in `products update-knowledge-tree`. URL filtering remains a permanent compatibility rule even though the current API drops URL from write payloads.
|
|
232
|
+
|
|
233
|
+
Removed forms include:
|
|
234
|
+
|
|
235
|
+
- `小荷`
|
|
236
|
+
- `小荷医疗`, including whitespace between `小荷` and `医疗`
|
|
237
|
+
- `小荷AI医生` with arbitrary whitespace, including `小荷 AI 医生`, `小荷 AI医生`, and `小荷AI 医生`
|
|
238
|
+
|
|
239
|
+
Whitespace is normalized after removal, and leading or trailing title separators are removed. If a supplied title becomes empty, the CLI returns `validation_error` without sending the request.
|
|
202
240
|
|
|
203
241
|
List generation tasks:
|
|
204
242
|
|
|
205
243
|
```bash
|
|
206
|
-
|
|
244
|
+
viatris-exam gen-tasks list \
|
|
207
245
|
--status generating \
|
|
208
246
|
--limit 20 \
|
|
209
247
|
--compact
|
|
@@ -214,7 +252,7 @@ node src/cli.js gen-tasks list \
|
|
|
214
252
|
Get one generation task:
|
|
215
253
|
|
|
216
254
|
```bash
|
|
217
|
-
|
|
255
|
+
viatris-exam gen-tasks get "$TASK_ID" --compact
|
|
218
256
|
```
|
|
219
257
|
|
|
220
258
|
For a leaf-node task, `knowledgeContext` is the mandatory target context. For a product-wide task, `dimension` is `all`, `knowledgeContext` is null, and `knowledgeContexts` contains every leaf context. Generate `questionsPerNode` questions for every listed context and follow all returned `rules`.
|
|
@@ -224,7 +262,7 @@ Each rule has `scope` and `products`. For `scope: "product"`, `products` contain
|
|
|
224
262
|
Submit generated questions:
|
|
225
263
|
|
|
226
264
|
```bash
|
|
227
|
-
|
|
265
|
+
viatris-exam gen-tasks submit "$TASK_ID" --input payload.json --compact
|
|
228
266
|
```
|
|
229
267
|
|
|
230
268
|
Payload shape:
|
|
@@ -295,13 +333,13 @@ Omit `knowledgeNodeId` for ordinary tasks. For a product-wide task it is require
|
|
|
295
333
|
Get a submitted exam record and its frozen answers:
|
|
296
334
|
|
|
297
335
|
```bash
|
|
298
|
-
|
|
336
|
+
viatris-exam exam-records get "$RECORD_ID" --compact
|
|
299
337
|
```
|
|
300
338
|
|
|
301
339
|
Get the complete scoring rules bound to the record:
|
|
302
340
|
|
|
303
341
|
```bash
|
|
304
|
-
|
|
342
|
+
viatris-exam exam-records scoring-rules "$RECORD_ID" --compact
|
|
305
343
|
```
|
|
306
344
|
|
|
307
345
|
The response includes the scoring `taskId`, `schemaVersion`, frozen question versions, evidence, V2 deduction rules, score caps, and item IDs. Use its `taskId` for scoring submission.
|
|
@@ -309,13 +347,13 @@ The response includes the scoring `taskId`, `schemaVersion`, frozen question ver
|
|
|
309
347
|
Get a scoring task:
|
|
310
348
|
|
|
311
349
|
```bash
|
|
312
|
-
|
|
350
|
+
viatris-exam scoring get "$TASK_ID" --compact
|
|
313
351
|
```
|
|
314
352
|
|
|
315
353
|
Submit scoring results:
|
|
316
354
|
|
|
317
355
|
```bash
|
|
318
|
-
|
|
356
|
+
viatris-exam scoring submit "$TASK_ID" --input results.json --compact
|
|
319
357
|
```
|
|
320
358
|
|
|
321
359
|
Payload shape:
|
|
@@ -347,7 +385,7 @@ For V2, submit exactly one `pointJudgement` per scoring point. IDs must match th
|
|
|
347
385
|
Create an exam:
|
|
348
386
|
|
|
349
387
|
```bash
|
|
350
|
-
|
|
388
|
+
viatris-exam exams create --input exam.json --compact
|
|
351
389
|
```
|
|
352
390
|
|
|
353
391
|
Required top-level fields are `name`, `product`, `questionCount`, `startAt`, `endAt`, `resultPolicy`, and `assigneeIds`. `questionCount` is 1 through 150. Use either `questions` or legacy `questionIds`, never both. Each question score must be a positive integer and the complete exam must total exactly 100.
|
|
@@ -355,7 +393,7 @@ Required top-level fields are `name`, `product`, `questionCount`, `startAt`, `en
|
|
|
355
393
|
Create questions directly:
|
|
356
394
|
|
|
357
395
|
```bash
|
|
358
|
-
|
|
396
|
+
viatris-exam questions create --input questions.json --compact
|
|
359
397
|
```
|
|
360
398
|
|
|
361
399
|
Payload shape is `{ "items": [...] }`. Each item requires `product`, `dimension`, and `content`; provide `externalRef` for idempotency. Prefer `scoringRuleVersion: 2`, stable IDs on scoring points and deduction rules, and explicit `scoreCapRules`.
|
|
@@ -365,7 +403,7 @@ The API has partial-success semantics: inspect `accepted`, `duplicates`, and eve
|
|
|
365
403
|
Add a question version:
|
|
366
404
|
|
|
367
405
|
```bash
|
|
368
|
-
|
|
406
|
+
viatris-exam questions version "$QUESTION_ID" --input version.json --compact
|
|
369
407
|
```
|
|
370
408
|
|
|
371
409
|
Required fields are `stem`, `referenceAnswer`, nonempty `scoringPoints`, `evidence`, and `editReason`. V2 versions may also include `scoringRuleVersion: 2`, deduction rules, and `scoreCapRules`.
|
|
@@ -373,7 +411,7 @@ Required fields are `stem`, `referenceAnswer`, nonempty `scoringPoints`, `eviden
|
|
|
373
411
|
Search medical evidence:
|
|
374
412
|
|
|
375
413
|
```bash
|
|
376
|
-
|
|
414
|
+
viatris-exam medical-search search "阿托伐他汀 适应症" \
|
|
377
415
|
--file-type drug \
|
|
378
416
|
--limit 20 \
|
|
379
417
|
--max-length 1200 \
|
|
@@ -384,7 +422,7 @@ Or use JSON:
|
|
|
384
422
|
|
|
385
423
|
```bash
|
|
386
424
|
printf '%s' '{"query":"阿托伐他汀 适应症","limit":20}' |
|
|
387
|
-
|
|
425
|
+
viatris-exam medical-search search --compact
|
|
388
426
|
```
|
|
389
427
|
|
|
390
428
|
Do not combine query flags with JSON input. `limit` accepts 1-20 and defaults to 20 when omitted. `maxLength` defaults to 1200.
|
|
@@ -396,10 +434,10 @@ The response `rawJson` is already a structured object. Read `rawJson.documents`
|
|
|
396
434
|
Choose exactly one input form:
|
|
397
435
|
|
|
398
436
|
```bash
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
printf '%s' '{"items":[]}' |
|
|
437
|
+
viatris-exam questions create --input payload.json
|
|
438
|
+
viatris-exam questions create --input - < payload.json
|
|
439
|
+
viatris-exam questions create --json '{"items":[]}'
|
|
440
|
+
printf '%s' '{"items":[]}' | viatris-exam questions create
|
|
403
441
|
```
|
|
404
442
|
|
|
405
443
|
The top-level JSON value must be an object.
|
|
Binary file
|
package/src/app.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
assertNoExtraPositionals,
|
|
3
|
+
assertOptions,
|
|
4
|
+
parseArgs,
|
|
5
|
+
} from "./args.js";
|
|
2
6
|
import { resolveCommand } from "./commands.js";
|
|
3
7
|
import { configFrom } from "./config.js";
|
|
8
|
+
import { loginWithEnvironmentKey } from "./credentials.js";
|
|
4
9
|
import { helpText } from "./help.js";
|
|
5
10
|
import { request } from "./http.js";
|
|
6
11
|
import { describeCli, VERSION } from "./metadata.js";
|
|
@@ -28,9 +33,16 @@ export async function run(
|
|
|
28
33
|
writeJson(stdout, describeCli(), options.compact);
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
36
|
+
if (positionals[0] === "login") {
|
|
37
|
+
assertNoExtraPositionals(positionals, 1);
|
|
38
|
+
assertOptions(options);
|
|
39
|
+
const result = await loginWithEnvironmentKey(environmentVariables);
|
|
40
|
+
writeJson(stdout, result, options.compact);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
31
43
|
|
|
32
44
|
const command = await resolveCommand(positionals, options);
|
|
33
|
-
const config = configFrom(options, environmentVariables);
|
|
45
|
+
const config = await configFrom(options, environmentVariables);
|
|
34
46
|
const response = await request(command, config);
|
|
35
47
|
writeJson(stdout, response, options.compact);
|
|
36
48
|
}
|
package/src/cli.js
CHANGED
|
File without changes
|
|
@@ -4,10 +4,23 @@ import {
|
|
|
4
4
|
requiredId,
|
|
5
5
|
} from "../args.js";
|
|
6
6
|
import { readJsonInput } from "../input.js";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
sanitizeDocumentBatchPayload,
|
|
9
|
+
sanitizeDocumentPayload,
|
|
10
|
+
} from "../sanitizers/documents.js";
|
|
11
|
+
|
|
12
|
+
async function jsonCommand(
|
|
13
|
+
method,
|
|
14
|
+
path,
|
|
15
|
+
options,
|
|
16
|
+
transform = (body) => body,
|
|
17
|
+
) {
|
|
9
18
|
assertOptions(options, ["input", "json"]);
|
|
10
|
-
return
|
|
19
|
+
return {
|
|
20
|
+
method,
|
|
21
|
+
path,
|
|
22
|
+
body: transform(await readJsonInput(options)),
|
|
23
|
+
};
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
function nodePath(positionals, label) {
|
|
@@ -96,6 +109,7 @@ export async function resolveKnowledge(positionals, options) {
|
|
|
96
109
|
"POST",
|
|
97
110
|
`/openapi/knowledge/nodes/${id}/documents`,
|
|
98
111
|
options,
|
|
112
|
+
sanitizeDocumentPayload,
|
|
99
113
|
);
|
|
100
114
|
}
|
|
101
115
|
|
|
@@ -106,6 +120,7 @@ export async function resolveKnowledge(positionals, options) {
|
|
|
106
120
|
"PUT",
|
|
107
121
|
`/openapi/knowledge/nodes/${id}/documents`,
|
|
108
122
|
options,
|
|
123
|
+
sanitizeDocumentBatchPayload,
|
|
109
124
|
);
|
|
110
125
|
}
|
|
111
126
|
|
|
@@ -116,6 +131,7 @@ export async function resolveKnowledge(positionals, options) {
|
|
|
116
131
|
"PUT",
|
|
117
132
|
`/openapi/knowledge/documents/${id}`,
|
|
118
133
|
options,
|
|
134
|
+
sanitizeDocumentPayload,
|
|
119
135
|
);
|
|
120
136
|
}
|
|
121
137
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
requiredId,
|
|
5
5
|
} from "../args.js";
|
|
6
6
|
import { readJsonInput } from "../input.js";
|
|
7
|
+
import { sanitizeKnowledgeTreePayload } from "../sanitizers/documents.js";
|
|
7
8
|
|
|
8
9
|
export async function resolveProducts(positionals, options) {
|
|
9
10
|
const [resource, action] = positionals;
|
|
@@ -50,7 +51,7 @@ export async function resolveProducts(positionals, options) {
|
|
|
50
51
|
return {
|
|
51
52
|
method: "PUT",
|
|
52
53
|
path: `/openapi/products/${id}/knowledge-tree`,
|
|
53
|
-
body: await readJsonInput(options),
|
|
54
|
+
body: sanitizeKnowledgeTreePayload(await readJsonInput(options)),
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
package/src/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { integerOption } from "./args.js";
|
|
2
|
+
import { readStoredApiKey } from "./credentials.js";
|
|
2
3
|
import { CliError } from "./errors.js";
|
|
3
4
|
import {
|
|
4
5
|
API_ENVIRONMENT_ALIASES,
|
|
@@ -7,7 +8,10 @@ import {
|
|
|
7
8
|
DEFAULT_TIMEOUT_MS,
|
|
8
9
|
} from "./metadata.js";
|
|
9
10
|
|
|
10
|
-
export function configFrom(
|
|
11
|
+
export async function configFrom(
|
|
12
|
+
options,
|
|
13
|
+
environmentVariables = process.env,
|
|
14
|
+
) {
|
|
11
15
|
const baseUrlOverride =
|
|
12
16
|
options.base_url ?? environmentVariables.VIATRIS_API_BASE_URL;
|
|
13
17
|
let environment = null;
|
|
@@ -28,13 +32,16 @@ export function configFrom(options, environmentVariables = process.env) {
|
|
|
28
32
|
baseUrl = API_ENVIRONMENTS[environment];
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
const
|
|
35
|
+
const storedApiKey = await readStoredApiKey(environmentVariables);
|
|
36
|
+
const apiKey = storedApiKey ?? environmentVariables.VIATRIS_API_KEY;
|
|
32
37
|
const keyHeader =
|
|
33
38
|
options.key_header ??
|
|
34
39
|
environmentVariables.VIATRIS_API_KEY_HEADER ??
|
|
35
|
-
"
|
|
40
|
+
"Authorization";
|
|
36
41
|
const keyPrefix =
|
|
37
|
-
options.key_prefix ??
|
|
42
|
+
options.key_prefix ??
|
|
43
|
+
environmentVariables.VIATRIS_API_KEY_PREFIX ??
|
|
44
|
+
"Bearer ";
|
|
38
45
|
const timeoutMs = integerOption(
|
|
39
46
|
options.timeout ?? String(DEFAULT_TIMEOUT_MS),
|
|
40
47
|
"timeout",
|
|
@@ -63,9 +70,11 @@ export function configFrom(options, environmentVariables = process.env) {
|
|
|
63
70
|
);
|
|
64
71
|
}
|
|
65
72
|
if (!apiKey) {
|
|
66
|
-
throw new CliError(
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
throw new CliError(
|
|
74
|
+
"config_error",
|
|
75
|
+
"API key not found. Run `viatris-exam login` with VIATRIS_API_KEY set, or set VIATRIS_API_KEY for this request.",
|
|
76
|
+
{ exitCode: 3 },
|
|
77
|
+
);
|
|
69
78
|
}
|
|
70
79
|
if (!/^[A-Za-z0-9-]+$/.test(keyHeader)) {
|
|
71
80
|
throw new CliError("config_error", "API key header name is invalid.", {
|
|
@@ -77,6 +86,7 @@ export function configFrom(options, environmentVariables = process.env) {
|
|
|
77
86
|
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
78
87
|
environment,
|
|
79
88
|
apiKey,
|
|
89
|
+
apiKeySource: storedApiKey === null ? "environment" : "credential_file",
|
|
80
90
|
keyHeader,
|
|
81
91
|
keyPrefix,
|
|
82
92
|
timeoutMs,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmod,
|
|
3
|
+
mkdir,
|
|
4
|
+
readFile,
|
|
5
|
+
rename,
|
|
6
|
+
rm,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from "node:fs/promises";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { CliError } from "./errors.js";
|
|
12
|
+
|
|
13
|
+
export const CREDENTIAL_FILE_DISPLAY =
|
|
14
|
+
"~/.viatris-exam/.credentials.json";
|
|
15
|
+
|
|
16
|
+
export function credentialFilePath(environmentVariables = process.env) {
|
|
17
|
+
const home = environmentVariables.HOME || homedir();
|
|
18
|
+
return join(home, ".viatris-exam", ".credentials.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function readStoredApiKey(
|
|
22
|
+
environmentVariables = process.env,
|
|
23
|
+
) {
|
|
24
|
+
const path = credentialFilePath(environmentVariables);
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await readFile(path, "utf8");
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === "ENOENT") return null;
|
|
30
|
+
throw new CliError(
|
|
31
|
+
"config_error",
|
|
32
|
+
`Unable to read the credential file: ${error.message}`,
|
|
33
|
+
{ exitCode: 3, details: { credentialFile: path } },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const credentials = JSON.parse(content);
|
|
39
|
+
if (
|
|
40
|
+
credentials === null ||
|
|
41
|
+
typeof credentials !== "object" ||
|
|
42
|
+
typeof credentials.apiKey !== "string" ||
|
|
43
|
+
!credentials.apiKey
|
|
44
|
+
) {
|
|
45
|
+
throw new Error("apiKey is missing or invalid");
|
|
46
|
+
}
|
|
47
|
+
return credentials.apiKey;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new CliError(
|
|
50
|
+
"config_error",
|
|
51
|
+
`Invalid credential file: ${error.message}`,
|
|
52
|
+
{ exitCode: 3, details: { credentialFile: path } },
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function loginWithEnvironmentKey(
|
|
58
|
+
environmentVariables = process.env,
|
|
59
|
+
) {
|
|
60
|
+
const apiKey = environmentVariables.VIATRIS_API_KEY;
|
|
61
|
+
if (!apiKey) {
|
|
62
|
+
throw new CliError(
|
|
63
|
+
"config_error",
|
|
64
|
+
"VIATRIS_API_KEY is required for login.",
|
|
65
|
+
{ exitCode: 3 },
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const path = credentialFilePath(environmentVariables);
|
|
70
|
+
const directory = dirname(path);
|
|
71
|
+
const temporaryPath = join(
|
|
72
|
+
directory,
|
|
73
|
+
`.credentials.${process.pid}.${Date.now()}.tmp`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
78
|
+
await chmod(directory, 0o700);
|
|
79
|
+
await writeFile(
|
|
80
|
+
temporaryPath,
|
|
81
|
+
`${JSON.stringify({ version: 1, apiKey }, null, 2)}\n`,
|
|
82
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
83
|
+
);
|
|
84
|
+
await rename(temporaryPath, path);
|
|
85
|
+
await chmod(path, 0o600);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
await rm(temporaryPath, { force: true });
|
|
88
|
+
throw new CliError(
|
|
89
|
+
"config_error",
|
|
90
|
+
`Unable to save the credential file: ${error.message}`,
|
|
91
|
+
{ exitCode: 3, details: { credentialFile: path } },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
status: "saved",
|
|
97
|
+
credentialFile: path,
|
|
98
|
+
permissions: "0600",
|
|
99
|
+
};
|
|
100
|
+
}
|
package/src/help.js
CHANGED
|
@@ -5,9 +5,11 @@ export function helpText() {
|
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
viatris-exam <resource> <action> [id|query] [options]
|
|
8
|
+
viatris-exam login
|
|
8
9
|
viatris-exam describe
|
|
9
10
|
|
|
10
11
|
Commands:
|
|
12
|
+
login
|
|
11
13
|
products list
|
|
12
14
|
products create --input <file|->
|
|
13
15
|
products get <product-id>
|
|
@@ -48,8 +50,8 @@ Medical search:
|
|
|
48
50
|
|
|
49
51
|
Global options:
|
|
50
52
|
--base-url <url> Override the selected environment URL.
|
|
51
|
-
--key-header <name> Override
|
|
52
|
-
--key-prefix <value> Override
|
|
53
|
+
--key-header <name> Override the auth header (default: Authorization).
|
|
54
|
+
--key-prefix <value> Override the auth prefix (default: "Bearer ").
|
|
53
55
|
--timeout <ms> Request timeout (default: 30000).
|
|
54
56
|
--retries <n> GET retries for 429/5xx/network errors (default: 2).
|
|
55
57
|
--compact Emit compact JSON.
|
|
@@ -59,9 +61,12 @@ Global options:
|
|
|
59
61
|
Environment:
|
|
60
62
|
VIATRIS_API_ENV API environment: production (default) or development.
|
|
61
63
|
VIATRIS_API_BASE_URL Optional URL override with highest priority.
|
|
62
|
-
VIATRIS_API_KEY
|
|
64
|
+
VIATRIS_API_KEY Login input or fallback key when no saved key exists.
|
|
63
65
|
VIATRIS_API_KEY_HEADER Optional authentication header name.
|
|
64
|
-
VIATRIS_API_KEY_PREFIX Optional header value prefix
|
|
66
|
+
VIATRIS_API_KEY_PREFIX Optional authentication header value prefix.
|
|
67
|
+
|
|
68
|
+
Credentials:
|
|
69
|
+
~/.viatris-exam/.credentials.json (read before VIATRIS_API_KEY, mode 0600)
|
|
65
70
|
|
|
66
71
|
Agent contract:
|
|
67
72
|
Successful API JSON is written to stdout. Structured errors are written to
|
package/src/metadata.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { CREDENTIAL_FILE_DISPLAY } from "./credentials.js";
|
|
2
|
+
import { DOCUMENT_SANITIZATION_POLICY } from "./sanitizers/documents.js";
|
|
3
|
+
|
|
4
|
+
export const VERSION = "1.8.1";
|
|
2
5
|
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
6
|
export const DEFAULT_RETRIES = 2;
|
|
4
7
|
|
|
@@ -15,6 +18,12 @@ export const API_ENVIRONMENT_ALIASES = Object.freeze({
|
|
|
15
18
|
});
|
|
16
19
|
|
|
17
20
|
export const COMMANDS = Object.freeze([
|
|
21
|
+
{
|
|
22
|
+
command: "login",
|
|
23
|
+
local: true,
|
|
24
|
+
input: "VIATRIS_API_KEY environment variable",
|
|
25
|
+
summary: "Save the API key to the local hidden credential file.",
|
|
26
|
+
},
|
|
18
27
|
{
|
|
19
28
|
command: "products list",
|
|
20
29
|
method: "GET",
|
|
@@ -113,7 +122,7 @@ export const COMMANDS = Object.freeze([
|
|
|
113
122
|
command: "knowledge documents create <node-id>",
|
|
114
123
|
method: "POST",
|
|
115
124
|
path: "/openapi/knowledge/nodes/{id}/documents",
|
|
116
|
-
input: "{title,
|
|
125
|
+
input: "{title, summary?}",
|
|
117
126
|
summary: "Idempotently create a document under a node.",
|
|
118
127
|
},
|
|
119
128
|
{
|
|
@@ -127,7 +136,7 @@ export const COMMANDS = Object.freeze([
|
|
|
127
136
|
command: "knowledge documents update <document-id>",
|
|
128
137
|
method: "PUT",
|
|
129
138
|
path: "/openapi/knowledge/documents/{id}",
|
|
130
|
-
input: "{title?,
|
|
139
|
+
input: "{title?, summary?}",
|
|
131
140
|
summary: "Update supplied fields on a knowledge document.",
|
|
132
141
|
},
|
|
133
142
|
{
|
|
@@ -235,7 +244,10 @@ export function describeCli() {
|
|
|
235
244
|
authentication: {
|
|
236
245
|
keyEnvironmentVariable: "VIATRIS_API_KEY",
|
|
237
246
|
baseUrlEnvironmentVariable: "VIATRIS_API_BASE_URL",
|
|
238
|
-
defaultHeader: "
|
|
247
|
+
defaultHeader: "Authorization",
|
|
248
|
+
defaultPrefix: "Bearer ",
|
|
249
|
+
credentialFile: CREDENTIAL_FILE_DISPLAY,
|
|
250
|
+
keySourcePriority: ["credential_file", "environment"],
|
|
239
251
|
},
|
|
240
252
|
environments: {
|
|
241
253
|
environmentVariable: "VIATRIS_API_ENV",
|
|
@@ -244,6 +256,7 @@ export function describeCli() {
|
|
|
244
256
|
urls: API_ENVIRONMENTS,
|
|
245
257
|
baseUrlOverrideEnvironmentVariable: "VIATRIS_API_BASE_URL",
|
|
246
258
|
},
|
|
259
|
+
documentSanitization: DOCUMENT_SANITIZATION_POLICY,
|
|
247
260
|
commands: COMMANDS,
|
|
248
261
|
exitCodes: EXIT_CODES,
|
|
249
262
|
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CliError } from "../errors.js";
|
|
2
|
+
|
|
3
|
+
export const DOCUMENT_SANITIZATION_POLICY = Object.freeze({
|
|
4
|
+
fields: ["title", "url", "summary"],
|
|
5
|
+
writableFields: ["title", "summary"],
|
|
6
|
+
droppedWriteFields: ["url"],
|
|
7
|
+
removedLabels: [
|
|
8
|
+
"小荷",
|
|
9
|
+
"小荷医疗",
|
|
10
|
+
"小荷AI医生(AI 与医生之间允许任意空格)",
|
|
11
|
+
],
|
|
12
|
+
appliesTo: [
|
|
13
|
+
"products update-knowledge-tree",
|
|
14
|
+
"knowledge documents create",
|
|
15
|
+
"knowledge documents upsert",
|
|
16
|
+
"knowledge documents update",
|
|
17
|
+
],
|
|
18
|
+
emptyTitle: "validation_error",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const PROHIBITED_LABEL = /小荷\s*(?:AI\s*医生|医疗)?/giu;
|
|
22
|
+
const TITLE_EDGE_SEPARATOR = /^[\s\-—–_::||·]+|[\s\-—–_::||·]+$/gu;
|
|
23
|
+
|
|
24
|
+
function sanitizeText(value) {
|
|
25
|
+
return value
|
|
26
|
+
.replace(PROHIBITED_LABEL, "")
|
|
27
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeTitle(value) {
|
|
32
|
+
return sanitizeText(value).replace(TITLE_EDGE_SEPARATOR, "").trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sanitizeDocument(document, location = "document") {
|
|
36
|
+
if (
|
|
37
|
+
document === null ||
|
|
38
|
+
Array.isArray(document) ||
|
|
39
|
+
typeof document !== "object"
|
|
40
|
+
) {
|
|
41
|
+
return document;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sanitized = { ...document };
|
|
45
|
+
if (typeof sanitized.title === "string") {
|
|
46
|
+
sanitized.title = sanitizeTitle(sanitized.title);
|
|
47
|
+
if (!sanitized.title) {
|
|
48
|
+
throw new CliError(
|
|
49
|
+
"validation_error",
|
|
50
|
+
`${location}.title is empty after prohibited source labels are removed.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const field of ["url", "summary"]) {
|
|
55
|
+
if (typeof sanitized[field] === "string") {
|
|
56
|
+
sanitized[field] = sanitizeText(sanitized[field]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return sanitized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function prepareDocumentForWrite(document, location) {
|
|
63
|
+
const sanitized = sanitizeDocument(document, location);
|
|
64
|
+
if (
|
|
65
|
+
sanitized === null ||
|
|
66
|
+
Array.isArray(sanitized) ||
|
|
67
|
+
typeof sanitized !== "object"
|
|
68
|
+
) {
|
|
69
|
+
return sanitized;
|
|
70
|
+
}
|
|
71
|
+
const { url: _unsupportedUrl, ...writableDocument } = sanitized;
|
|
72
|
+
return writableDocument;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function sanitizeDocumentPayload(body) {
|
|
76
|
+
return prepareDocumentForWrite(body, "document");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function sanitizeDocumentBatchPayload(body) {
|
|
80
|
+
if (!Array.isArray(body.items)) return body;
|
|
81
|
+
return {
|
|
82
|
+
...body,
|
|
83
|
+
items: body.items.map((document, index) =>
|
|
84
|
+
prepareDocumentForWrite(document, `items[${index}]`),
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function sanitizeKnowledgeTreePayload(body) {
|
|
90
|
+
if (!Array.isArray(body.nodes)) return body;
|
|
91
|
+
return {
|
|
92
|
+
...body,
|
|
93
|
+
nodes: body.nodes.map((node, nodeIndex) => {
|
|
94
|
+
if (
|
|
95
|
+
node === null ||
|
|
96
|
+
Array.isArray(node) ||
|
|
97
|
+
typeof node !== "object" ||
|
|
98
|
+
!Array.isArray(node.documents)
|
|
99
|
+
) {
|
|
100
|
+
return node;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
...node,
|
|
104
|
+
documents: node.documents.map((document, documentIndex) =>
|
|
105
|
+
prepareDocumentForWrite(
|
|
106
|
+
document,
|
|
107
|
+
`nodes[${nodeIndex}].documents[${documentIndex}]`,
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
}
|