dsh-d365-kit 0.34.1 → 0.34.3
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/CHANGELOG.md +318 -308
- package/CHANGELOG.zh-CN.md +317 -307
- package/INSTALL.md +94 -94
- package/INSTALL.zh-CN.md +93 -93
- package/LICENSE +21 -21
- package/README.md +336 -327
- package/README.zh-CN.md +336 -327
- package/client.js +1818 -1818
- package/cordis.patch.yml +44 -44
- package/docs/en/architecture.md +172 -172
- package/docs/en/checks.md +184 -184
- package/docs/en/installation.md +151 -151
- package/docs/en/permissions.md +161 -161
- package/docs/en/scenarios.md +202 -202
- package/docs/en/tools.md +665 -665
- package/docs/en/troubleshooting.md +131 -131
- package/docs/zh/architecture.md +172 -172
- package/docs/zh/checks.md +184 -184
- package/docs/zh/installation.md +151 -151
- package/docs/zh/permissions.md +161 -161
- package/docs/zh/scenarios.md +202 -202
- package/docs/zh/tools.md +665 -665
- package/docs/zh/troubleshooting.md +131 -131
- package/examples/prompts.en.md +206 -206
- package/examples/prompts.zh-CN.md +206 -206
- package/examples/waivers.example.json +20 -20
- package/icon.svg +6 -6
- package/index.js +139 -139
- package/lib/auth.js +181 -181
- package/lib/client.js +496 -496
- package/lib/code-check.js +549 -549
- package/lib/config.js +323 -323
- package/lib/contract-history.js +49 -49
- package/lib/contract.js +583 -583
- package/lib/csharp-check.js +493 -493
- package/lib/data-migrate.js +1318 -1318
- package/lib/data-policy.js +78 -78
- package/lib/error-codes.js +80 -80
- package/lib/gitlab.js +161 -161
- package/lib/health.js +205 -205
- package/lib/migrate.js +732 -732
- package/lib/preflight.js +116 -116
- package/lib/refs.js +191 -191
- package/lib/resources.js +320 -320
- package/lib/retention-defaults.js +17 -17
- package/lib/retention.js +384 -384
- package/lib/runtime.js +232 -232
- package/lib/settings-http.js +512 -512
- package/lib/settings-store.js +371 -371
- package/lib/snapshot.js +149 -149
- package/lib/tool-code-check.js +133 -133
- package/lib/tool-configure.js +169 -169
- package/lib/tool-contract.js +376 -376
- package/lib/tool-data-sync.js +559 -559
- package/lib/tool-data-write.js +203 -203
- package/lib/tool-data.js +282 -282
- package/lib/tool-diff.js +484 -484
- package/lib/tool-environments.js +106 -106
- package/lib/tool-flows.js +540 -540
- package/lib/tool-health.js +127 -127
- package/lib/tool-layers.js +497 -497
- package/lib/tool-metadata.js +417 -417
- package/lib/tool-migrate.js +102 -102
- package/lib/tool-refs.js +206 -206
- package/lib/tool-reports.js +299 -299
- package/lib/tool-sync.js +430 -430
- package/lib/tool-write.js +563 -563
- package/lib/waivers.js +83 -83
- package/lib/write-actions.js +1374 -1374
- package/lib/write-policy.js +199 -199
- package/lib/write-store.js +167 -167
- package/locale/en.json +68 -68
- package/locale/zh.json +68 -68
- package/package.json +2 -2
- package/skills/d365-troubleshooting/SKILL.md +115 -115
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
# Troubleshooting
|
|
2
|
-
|
|
3
|
-
**English** | [简体中文](../zh/troubleshooting.md)
|
|
4
|
-
|
|
5
|
-
## Do these three things first
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
plugin_manager list_plugins
|
|
9
|
-
d365_envs check=true
|
|
10
|
-
d365_health allEnvironments=true
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
The first shows whether the plugin is present and active; the second checks credentials and network; the third shows whether the environment is busy. Most "tool errors" can be pinned down in these three steps.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Error code reference
|
|
18
|
-
|
|
19
|
-
| Error / symptom | Meaning | Fix |
|
|
20
|
-
| --- | --- | --- |
|
|
21
|
-
| `unknown-environment` | The environment key does not exist | Use `d365_envs` to list available keys; check the spelling on the settings page |
|
|
22
|
-
| AADSTS series / `invalid_client` | Wrong credentials, expired secret, or application not authorized | Regenerate the secret; confirm the application user exists in **this** environment |
|
|
23
|
-
| `metadata-read-forbidden` | "Read metadata" is turned off for this environment | Settings page → Permissions → tick it back (on by default; this only fires when it was explicitly revoked) |
|
|
24
|
-
| `data-read-forbidden` | This environment is not authorized to read business data | Settings page → Permissions → enable "Read business data" |
|
|
25
|
-
| `data-write-forbidden` | Not authorized to write business data | Enable "Create/Edit business data"; every write still needs confirmation |
|
|
26
|
-
| `environment-busy` | The environment is importing/publishing (`msdyn_solutionhistories` has an unfinished record) | Wait for it to finish; use `force: true` only after confirming it is a leftover |
|
|
27
|
-
| `plan-not-pending` | The plan is executing / was interrupted last time / has already run | Regenerate the plan; writes are not idempotent, so do not rerun an old plan |
|
|
28
|
-
| `unknown-solution` / `managed-solution` | The default solution does not exist or is managed | Switch to an **existing unmanaged** solution |
|
|
29
|
-
| `destructive-not-acknowledged` | Deleting a field/option value without passing `acknowledge: true` | Confirm explicitly, then pass it |
|
|
30
|
-
| `option-set-unreadable` | The field's local option set cannot be read (all type conversions failed) | Confirm the field really is an option set; read the hint in the response |
|
|
31
|
-
| `unknown-view` / `ambiguous-view` | The view name does not exist / several share the name | Pass `entity` to disambiguate |
|
|
32
|
-
| `HTTP 400 … Could not find a property named` | The environment lacks that column, or the column name does not fit this endpoint | The tool attempts degradation; if it still fails, use `select` to name existing columns explicitly |
|
|
33
|
-
| `0x80060888 … query parameter is not supported` | The endpoint rejects that parameter (e.g. `$top`/`$orderby` on `EntityDefinitions`) | The client already degrades automatically; keep this in mind when writing your own scripts |
|
|
34
|
-
| `0x80071151` | The customization lock is held (an import/publish is running) | Confirm with `d365_health` and wait it out; do not retry in a loop |
|
|
35
|
-
| `HTTP 429` | Service protection (about 6,000 requests / 5 minutes) | Lower `concurrency` (≤ 4) and retry later; only write requests retry automatically, and only on 429 |
|
|
36
|
-
| Publish times out at 60–300 s | `PublishAllXml` is slow on an org with 2,000-plus tables | Already switched to `PublishXml` per affected component; if it still times out, check `d365_health` |
|
|
37
|
-
| `GitLab 403` | Token scope is insufficient, or the user is below Reporter on the project | Use another personal access token or raise the role; Guest cannot read the repo or MRs |
|
|
38
|
-
| `GitLab 404` | Wrong project path or IID, or the token cannot see the project | Use the full MR link, or fill in the default project |
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## Install / loading problems
|
|
43
|
-
|
|
44
|
-
### Tools do not appear
|
|
45
|
-
|
|
46
|
-
1. Run `plugin_manager list_plugins`, find `include:dsh-d365-kit`, and confirm `enabled=true` and `fiberPhase=active`;
|
|
47
|
-
2. Restart DSH right after installing (Web client plugins have a module graph cache);
|
|
48
|
-
3. If the entry is missing: `remove_bundle` then `install_bundle` (when link/pnpm thinks the package already exists it only returns `already-installed` and does not replace the code).
|
|
49
|
-
|
|
50
|
-
### Restart reports `duplicate factory registration` / `import failed`
|
|
51
|
-
|
|
52
|
-
The same bundle was executed twice. Check whether the name in these two places matches the package name:
|
|
53
|
-
|
|
54
|
-
- `window.__ModuleLoader__.load({ id: ... })` at the top of `client.js`
|
|
55
|
-
- `export const name = ...` in `index.js`
|
|
56
|
-
|
|
57
|
-
Renaming the module or the package makes it easy to miss these two "self-reported names". If it still reproduces, quit DSH completely, delete Electron's compiled caches, then start again:
|
|
58
|
-
|
|
59
|
-
```
|
|
60
|
-
%APPDATA%\@deepseek-ai\dsh-desktop\Code Cache
|
|
61
|
-
%APPDATA%\@deepseek-ai\dsh-desktop\Local Storage
|
|
62
|
-
%APPDATA%\@deepseek-ai\dsh-desktop\Session Storage
|
|
63
|
-
%APPDATA%\@deepseek-ai\dsh-desktop\Cache
|
|
64
|
-
%APPDATA%\@deepseek-ai\dsh-desktop\GPUCache
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Only UI preferences are lost; sessions and config under `.dsh` are untouched.
|
|
68
|
-
|
|
69
|
-
### The settings page has no "D365 Toolbox"
|
|
70
|
-
|
|
71
|
-
The Host side may have loaded while the client half never made it into the module graph: restart once. If it is still missing, check whether the module id in `client.js` matches the package name (see above).
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## Settings page errors
|
|
76
|
-
|
|
77
|
-
| Symptom | Cause | Fix |
|
|
78
|
-
| --- | --- | --- |
|
|
79
|
-
| Saving permissions reports "writes can only be initiated from the settings page" | The request lacks `x-d365-meta-write: 1`, or comes from another origin | Only act from the settings page; rejections are logged in `.d365/write-guard.log` |
|
|
80
|
-
| Opening a tab returns `HTTP 400` with no details | The route did not declare a request body schema, so the connection layer built a GET with a body | Upgrade to 0.18.0+ (fixed); older bundles need a restart |
|
|
81
|
-
| Config is unchanged after saving | `configFile` points somewhere else | The bottom of the settings page shows the config file path; pin it with `D365_CONFIG_FILE` or a patch |
|
|
82
|
-
| English appears in a Chinese UI | The DSH UI language is English | Expected: page copy follows DSH |
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
## Data problems
|
|
87
|
-
|
|
88
|
-
### A layer query returns 0 rows
|
|
89
|
-
|
|
90
|
-
`msdyn_componentlayer` is a virtual entity: you **must supply both the component id and the type**. Giving only the name, only the id, or no filter at all silently returns 0 rows (no error).
|
|
91
|
-
|
|
92
|
-
In some environments the table is readable but the application user cannot see the data; administrator identity is required.
|
|
93
|
-
|
|
94
|
-
### Snapshots are large
|
|
95
|
-
|
|
96
|
-
A single full snapshot is about 125 MB (200k solution components, 94k plugin steps, 66k web resources). The default keeps 2 per environment; `d365_reports action=compact` collapses the indented JSON onto a single line (measured: saves 68 MB).
|
|
97
|
-
|
|
98
|
-
### Diff reports are noisy
|
|
99
|
-
|
|
100
|
-
Narrow it with `resources` first; `organization` is skipped by default, and timestamps and `*id` columns do not participate in the comparison. Use logical names when comparing across environments.
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## Write problems
|
|
105
|
-
|
|
106
|
-
### After execution it reports "plan does not exist"
|
|
107
|
-
|
|
108
|
-
The plan file is deleted once execution succeeds. Clicking an old card again or re-executing gives this message; check the recent change records under "Plans" on the settings page to confirm the result.
|
|
109
|
-
|
|
110
|
-
### The write succeeded but downstream cannot see it
|
|
111
|
-
|
|
112
|
-
1. Did the change land in the active default layer? Fill in "change default solution", otherwise exporting the solution will not carry it;
|
|
113
|
-
2. Is it covered downstream by another managed solution's layer? Check with `d365_layers`;
|
|
114
|
-
3. Was it published? Writes publish the affected components automatically; the result is in `.d365/changes/publish-<planId>.json`.
|
|
115
|
-
|
|
116
|
-
### The publish stays `running`
|
|
117
|
-
|
|
118
|
-
Publishing runs in the background and the sidecar file is written only when it finishes. Check `d365_health` to see whether that environment is still publishing; once nothing is active, rerun `PublishXml` — publishing itself is idempotent (a repeat publish does not change the result).
|
|
119
|
-
|
|
120
|
-
---
|
|
121
|
-
|
|
122
|
-
## Still stuck
|
|
123
|
-
|
|
124
|
-
Collect these before asking:
|
|
125
|
-
|
|
126
|
-
```
|
|
127
|
-
d365_envs
|
|
128
|
-
d365_health environments=["<that environment>"]
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Plus the full error text, the timestamp, and `.d365/write-guard.log` (for settings-page write problems).
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
**English** | [简体中文](../zh/troubleshooting.md)
|
|
4
|
+
|
|
5
|
+
## Do these three things first
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
plugin_manager list_plugins
|
|
9
|
+
d365_envs check=true
|
|
10
|
+
d365_health allEnvironments=true
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The first shows whether the plugin is present and active; the second checks credentials and network; the third shows whether the environment is busy. Most "tool errors" can be pinned down in these three steps.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Error code reference
|
|
18
|
+
|
|
19
|
+
| Error / symptom | Meaning | Fix |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `unknown-environment` | The environment key does not exist | Use `d365_envs` to list available keys; check the spelling on the settings page |
|
|
22
|
+
| AADSTS series / `invalid_client` | Wrong credentials, expired secret, or application not authorized | Regenerate the secret; confirm the application user exists in **this** environment |
|
|
23
|
+
| `metadata-read-forbidden` | "Read metadata" is turned off for this environment | Settings page → Permissions → tick it back (on by default; this only fires when it was explicitly revoked) |
|
|
24
|
+
| `data-read-forbidden` | This environment is not authorized to read business data | Settings page → Permissions → enable "Read business data" |
|
|
25
|
+
| `data-write-forbidden` | Not authorized to write business data | Enable "Create/Edit business data"; every write still needs confirmation |
|
|
26
|
+
| `environment-busy` | The environment is importing/publishing (`msdyn_solutionhistories` has an unfinished record) | Wait for it to finish; use `force: true` only after confirming it is a leftover |
|
|
27
|
+
| `plan-not-pending` | The plan is executing / was interrupted last time / has already run | Regenerate the plan; writes are not idempotent, so do not rerun an old plan |
|
|
28
|
+
| `unknown-solution` / `managed-solution` | The default solution does not exist or is managed | Switch to an **existing unmanaged** solution |
|
|
29
|
+
| `destructive-not-acknowledged` | Deleting a field/option value without passing `acknowledge: true` | Confirm explicitly, then pass it |
|
|
30
|
+
| `option-set-unreadable` | The field's local option set cannot be read (all type conversions failed) | Confirm the field really is an option set; read the hint in the response |
|
|
31
|
+
| `unknown-view` / `ambiguous-view` | The view name does not exist / several share the name | Pass `entity` to disambiguate |
|
|
32
|
+
| `HTTP 400 … Could not find a property named` | The environment lacks that column, or the column name does not fit this endpoint | The tool attempts degradation; if it still fails, use `select` to name existing columns explicitly |
|
|
33
|
+
| `0x80060888 … query parameter is not supported` | The endpoint rejects that parameter (e.g. `$top`/`$orderby` on `EntityDefinitions`) | The client already degrades automatically; keep this in mind when writing your own scripts |
|
|
34
|
+
| `0x80071151` | The customization lock is held (an import/publish is running) | Confirm with `d365_health` and wait it out; do not retry in a loop |
|
|
35
|
+
| `HTTP 429` | Service protection (about 6,000 requests / 5 minutes) | Lower `concurrency` (≤ 4) and retry later; only write requests retry automatically, and only on 429 |
|
|
36
|
+
| Publish times out at 60–300 s | `PublishAllXml` is slow on an org with 2,000-plus tables | Already switched to `PublishXml` per affected component; if it still times out, check `d365_health` |
|
|
37
|
+
| `GitLab 403` | Token scope is insufficient, or the user is below Reporter on the project | Use another personal access token or raise the role; Guest cannot read the repo or MRs |
|
|
38
|
+
| `GitLab 404` | Wrong project path or IID, or the token cannot see the project | Use the full MR link, or fill in the default project |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Install / loading problems
|
|
43
|
+
|
|
44
|
+
### Tools do not appear
|
|
45
|
+
|
|
46
|
+
1. Run `plugin_manager list_plugins`, find `include:dsh-d365-kit`, and confirm `enabled=true` and `fiberPhase=active`;
|
|
47
|
+
2. Restart DSH right after installing (Web client plugins have a module graph cache);
|
|
48
|
+
3. If the entry is missing: `remove_bundle` then `install_bundle` (when link/pnpm thinks the package already exists it only returns `already-installed` and does not replace the code).
|
|
49
|
+
|
|
50
|
+
### Restart reports `duplicate factory registration` / `import failed`
|
|
51
|
+
|
|
52
|
+
The same bundle was executed twice. Check whether the name in these two places matches the package name:
|
|
53
|
+
|
|
54
|
+
- `window.__ModuleLoader__.load({ id: ... })` at the top of `client.js`
|
|
55
|
+
- `export const name = ...` in `index.js`
|
|
56
|
+
|
|
57
|
+
Renaming the module or the package makes it easy to miss these two "self-reported names". If it still reproduces, quit DSH completely, delete Electron's compiled caches, then start again:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
%APPDATA%\@deepseek-ai\dsh-desktop\Code Cache
|
|
61
|
+
%APPDATA%\@deepseek-ai\dsh-desktop\Local Storage
|
|
62
|
+
%APPDATA%\@deepseek-ai\dsh-desktop\Session Storage
|
|
63
|
+
%APPDATA%\@deepseek-ai\dsh-desktop\Cache
|
|
64
|
+
%APPDATA%\@deepseek-ai\dsh-desktop\GPUCache
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Only UI preferences are lost; sessions and config under `.dsh` are untouched.
|
|
68
|
+
|
|
69
|
+
### The settings page has no "D365 Toolbox"
|
|
70
|
+
|
|
71
|
+
The Host side may have loaded while the client half never made it into the module graph: restart once. If it is still missing, check whether the module id in `client.js` matches the package name (see above).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Settings page errors
|
|
76
|
+
|
|
77
|
+
| Symptom | Cause | Fix |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| Saving permissions reports "writes can only be initiated from the settings page" | The request lacks `x-d365-meta-write: 1`, or comes from another origin | Only act from the settings page; rejections are logged in `.d365/write-guard.log` |
|
|
80
|
+
| Opening a tab returns `HTTP 400` with no details | The route did not declare a request body schema, so the connection layer built a GET with a body | Upgrade to 0.18.0+ (fixed); older bundles need a restart |
|
|
81
|
+
| Config is unchanged after saving | `configFile` points somewhere else | The bottom of the settings page shows the config file path; pin it with `D365_CONFIG_FILE` or a patch |
|
|
82
|
+
| English appears in a Chinese UI | The DSH UI language is English | Expected: page copy follows DSH |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Data problems
|
|
87
|
+
|
|
88
|
+
### A layer query returns 0 rows
|
|
89
|
+
|
|
90
|
+
`msdyn_componentlayer` is a virtual entity: you **must supply both the component id and the type**. Giving only the name, only the id, or no filter at all silently returns 0 rows (no error).
|
|
91
|
+
|
|
92
|
+
In some environments the table is readable but the application user cannot see the data; administrator identity is required.
|
|
93
|
+
|
|
94
|
+
### Snapshots are large
|
|
95
|
+
|
|
96
|
+
A single full snapshot is about 125 MB (200k solution components, 94k plugin steps, 66k web resources). The default keeps 2 per environment; `d365_reports action=compact` collapses the indented JSON onto a single line (measured: saves 68 MB).
|
|
97
|
+
|
|
98
|
+
### Diff reports are noisy
|
|
99
|
+
|
|
100
|
+
Narrow it with `resources` first; `organization` is skipped by default, and timestamps and `*id` columns do not participate in the comparison. Use logical names when comparing across environments.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Write problems
|
|
105
|
+
|
|
106
|
+
### After execution it reports "plan does not exist"
|
|
107
|
+
|
|
108
|
+
The plan file is deleted once execution succeeds. Clicking an old card again or re-executing gives this message; check the recent change records under "Plans" on the settings page to confirm the result.
|
|
109
|
+
|
|
110
|
+
### The write succeeded but downstream cannot see it
|
|
111
|
+
|
|
112
|
+
1. Did the change land in the active default layer? Fill in "change default solution", otherwise exporting the solution will not carry it;
|
|
113
|
+
2. Is it covered downstream by another managed solution's layer? Check with `d365_layers`;
|
|
114
|
+
3. Was it published? Writes publish the affected components automatically; the result is in `.d365/changes/publish-<planId>.json`.
|
|
115
|
+
|
|
116
|
+
### The publish stays `running`
|
|
117
|
+
|
|
118
|
+
Publishing runs in the background and the sidecar file is written only when it finishes. Check `d365_health` to see whether that environment is still publishing; once nothing is active, rerun `PublishXml` — publishing itself is idempotent (a repeat publish does not change the result).
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Still stuck
|
|
123
|
+
|
|
124
|
+
Collect these before asking:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
d365_envs
|
|
128
|
+
d365_health environments=["<that environment>"]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Plus the full error text, the timestamp, and `.d365/write-guard.log` (for settings-page write problems).
|
package/docs/zh/architecture.md
CHANGED
|
@@ -1,172 +1,172 @@
|
|
|
1
|
-
# 结构与开发
|
|
2
|
-
|
|
3
|
-
**[English](../en/architecture.md)** | 简体中文
|
|
4
|
-
|
|
5
|
-
## 形态
|
|
6
|
-
|
|
7
|
-
一个 DSH bundle,两部分:
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
index.js # Host 侧:注册 19 个工具、挂设置页的 HTTP 路由
|
|
11
|
-
client.js # Web 侧:设置页(六个标签,无构建步骤,直接是浏览器模块)
|
|
12
|
-
lib/*.js # Host 侧全部逻辑(41 个模块)
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
零运行时依赖:只用 Node 内置的 `fetch` / `fs` / `path`。没有 TypeScript、没有打包器、没有 `node_modules`。
|
|
16
|
-
|
|
17
|
-
`cordis.patch.yml` 声明 bundle 与默认配置(`configFile`、`timeoutMs`、`snapshotKeep`、`environments`);
|
|
18
|
-
`package.json` 的 `dsh.bundle.patch` 指向它,`dsh.client` 声明 Web 侧入口。
|
|
19
|
-
|
|
20
|
-
## 模块分工
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
lib/
|
|
24
|
-
client.js Dataverse Web API 客户端:拼 URL、分页、重试、权限闸、令牌
|
|
25
|
-
auth.js Entra ID 客户端凭据流;令牌按凭据指纹缓存
|
|
26
|
-
config.js 配置加载/归一化;configFile 回退链;密钥解析
|
|
27
|
-
resources.js 19 类元数据资源的查询定义与字段映射
|
|
28
|
-
runtime.js makeClient(env, config)、组织信息、版本
|
|
29
|
-
snapshot.js 快照写入、manifest、快照目录
|
|
30
|
-
retention.js 保留策略、磁盘统计、清理、压缩
|
|
31
|
-
write-policy.js 能力表(CAPABILITIES)与按环境的授权判定
|
|
32
|
-
write-actions.js 全部写操作的 planner 与 executor(唯一改环境的代码)
|
|
33
|
-
write-store.js 计划与变更记录的读写、计划状态机
|
|
34
|
-
preflight.js 执行前空闲检查(msdyn_solutionhistories)
|
|
35
|
-
settings-store.js 设置页的数据层(环境、权限、常规参数、GitLab)
|
|
36
|
-
settings-http.js 设置页路由(认证 channel 上的精确路径)
|
|
37
|
-
contract.js 契约事实读取、比对、渲染
|
|
38
|
-
migrate.js 迁移评估(逐组件查层)
|
|
39
|
-
code-check.js MR 检查(JS/TS)
|
|
40
|
-
csharp-check.js MR 检查(C#)
|
|
41
|
-
refs.js 反向索引(表单/视图/流程/步骤)
|
|
42
|
-
health.js 环境健康
|
|
43
|
-
gitlab.js GitLab API(MR diff、连通性)
|
|
44
|
-
waivers.js 契约豁免
|
|
45
|
-
contract-history.js 漂移趋势
|
|
46
|
-
data-migrate.js 业务记录跨环境迁移(计划、lookup 解析、写入、run 记录)
|
|
47
|
-
tool-*.js 19 个工具,一个文件一个工具(或一组相关的)
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## 数据流
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
agent 调 d365_* ──> tool-*.js ──> lib 能力模块 ──> client.js ──> Dataverse Web API
|
|
54
|
-
│ │
|
|
55
|
-
└────────── 摘要 + 文件路径 <───────────────┘
|
|
56
|
-
│
|
|
57
|
-
.d365/{snapshots,diffs,reports,query,changes}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
设计原则:
|
|
61
|
-
|
|
62
|
-
1. **工具返回摘要与路径,正文落文件**——对话里不塞几十 MB 的 JSON;
|
|
63
|
-
2. **只读优先**——只有 `d365_write execute` 与 `d365_data_write` 会改环境,且必须人批准;
|
|
64
|
-
3. **判定收在一处**——能看什么/能改什么都在 `lib/client.js` 的请求入口判定,新工具无法绕过;
|
|
65
|
-
4. **实时优于快照**——漂移、迁移、健康这类「现在怎么样」的问题读实时;快照只用于跨时间/跨环境比较;
|
|
66
|
-
5. **规则是数据**——契约规则、保留策略、能力表都是表驱动,加规则不改流程。
|
|
67
|
-
|
|
68
|
-
## 一次请求的路径
|
|
69
|
-
|
|
70
|
-
以 `d365_metadata resource=attributes` 为例:
|
|
71
|
-
|
|
72
|
-
```
|
|
73
|
-
tool-metadata.js
|
|
74
|
-
→ pickEnvironments(args) 选环境
|
|
75
|
-
→ loadConfig(state.rawConfig) 读配置(每个工具入口都做)
|
|
76
|
-
→ makeClient(env, config) 建客户端
|
|
77
|
-
→ resources.js 的查询定义 拼 path/params
|
|
78
|
-
→ client.getPagedWithSelectFallback 分页 + 列名降级
|
|
79
|
-
→ 权限闸(元数据实体集 → metadata.read;其余 GET → data.read;写 → data.write)
|
|
80
|
-
→ getAccessToken(按凭据指纹缓存)
|
|
81
|
-
→ fetch,429/5xx 按方法决定是否重试
|
|
82
|
-
→ 跨环境对齐 + 摘要
|
|
83
|
-
→ 超过 12 行写 .d365/query/*.json
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## 加一个工具
|
|
87
|
-
|
|
88
|
-
```js
|
|
89
|
-
// lib/tool-demo.js
|
|
90
|
-
import { pickEnvironments } from './tool-metadata.js'; // 或自己实现
|
|
91
|
-
|
|
92
|
-
export default function registerDemo(ctx, state) {
|
|
93
|
-
const disposer = ctx.tools.register({
|
|
94
|
-
name: 'd365_demo',
|
|
95
|
-
description: '一句话说明它回答什么问题。',
|
|
96
|
-
parameters: { // 必须是标准 JSON Schema
|
|
97
|
-
type: 'object',
|
|
98
|
-
additionalProperties: false,
|
|
99
|
-
required: ['entity'],
|
|
100
|
-
properties: {
|
|
101
|
-
entity: { type: 'string', description: '实体逻辑名' },
|
|
102
|
-
environment: { type: 'string', description: '环境 key' },
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
output: {
|
|
106
|
-
schema: { // 同样必须是标准 JSON Schema
|
|
107
|
-
type: 'object',
|
|
108
|
-
additionalProperties: false,
|
|
109
|
-
required: ['text'],
|
|
110
|
-
properties: { text: { type: 'string' } },
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
async execute(args, exec) {
|
|
114
|
-
const config = await loadConfig(state.rawConfig); // 注意:rawConfig
|
|
115
|
-
...
|
|
116
|
-
return { text };
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
ctx.effect(() => disposer);
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
然后 `index.js` 里 import、调用 `registerDemo(ctx, state)`、把 disposer 加进列表。
|
|
124
|
-
|
|
125
|
-
DSH 的硬性要求(都踩过):
|
|
126
|
-
|
|
127
|
-
| 要求 | 症状 |
|
|
128
|
-
| --- | --- |
|
|
129
|
-
| `parameters` 与 `output.schema` 都是标准 JSON Schema(根节点 `type: 'object'`,`required` 是数组) | `schema must be a JSON Schema of 'type: "object"', got 'type: null'` |
|
|
130
|
-
| 输出严格符合 `output.schema`,不能多字段、不能 `type: ['string','null']` | `value.x is not a declared property` / `type arrays are not supported` |
|
|
131
|
-
| 配置从 `state.rawConfig` 取(`index.js` 传的是 `{ rawConfig }`) | 环境莫名不存在、列表为空 |
|
|
132
|
-
| Host 侧改完要重载插件 | 工具行为还是旧的 |
|
|
133
|
-
|
|
134
|
-
## 客户端(设置页)
|
|
135
|
-
|
|
136
|
-
`client.js` 是一个浏览器模块:`window.__ModuleLoader__.load({ id: 'dsh-d365-kit', factory })`。
|
|
137
|
-
|
|
138
|
-
- 六个标签的面板**同时挂载**,用 `display:none` 隐藏非活动页——切标签不丢编辑状态;
|
|
139
|
-
- 文案内置中英字典,`t()` 每次按 `localeService.getLocale().active` 现算,不依赖 `locale.register` 成功;
|
|
140
|
-
- 语言切换通过订阅 locale 变化触发重渲染;
|
|
141
|
-
- 所有写操作带 `x-d365-meta-write: 1`(Host 侧据此拒绝非设置页请求)。
|
|
142
|
-
|
|
143
|
-
**模块 id 必须与包名一致**,否则加载器会把它当两个模块执行两次,报 `duplicate factory registration` 并让整个设置页消失。
|
|
144
|
-
|
|
145
|
-
## 本地验证
|
|
146
|
-
|
|
147
|
-
没有环境也能测大部分逻辑:stub 掉 `globalThis.fetch` 与 `ctx.tools`,直接调模块函数。
|
|
148
|
-
|
|
149
|
-
```js
|
|
150
|
-
globalThis.fetch = async (url) => new Response(JSON.stringify({ value: [] }), { status: 200 });
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
注意:`new Response('', { status: 204 })` 在 Node 里会抛 `Invalid response status code 204`,204 要用 `null` body。
|
|
154
|
-
|
|
155
|
-
真实环境验证的最小集:
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
d365_envs check=true
|
|
159
|
-
d365_metadata resource=entities top=2
|
|
160
|
-
d365_layers component=account componentType=Entity
|
|
161
|
-
d365_health environments=["<env>"]
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## 已知的坑(写给下一个改代码的人)
|
|
165
|
-
|
|
166
|
-
- 元数据端点不接受 `$top` / `$orderby`,客户端有渐进降级;写新查询时优先用 `getPagedWithSelectFallback`;
|
|
167
|
-
- `getPagedWithSelectFallback` 返回 `{ page, selectDropped, ... }`,行在 `result.page.records`;
|
|
168
|
-
- `odataString()` 只转义引号、**不包引号**,过滤器里要自己写 `'...'`;
|
|
169
|
-
- `client.getOne()` 会附加 `$top=1`,对 `solutions` / `solutioncomponents` 会 400,改用 `requestJson` 手动翻页;
|
|
170
|
-
- 层表、`msdyn_solutionhistories` 这类虚拟/审计表的行为与普通表不同,先小样本验证;
|
|
171
|
-
- 含非 ASCII 的文件不要用 PowerShell 5.1 的 `Get-Content | Set-Content` 改写(会按 ANSI 二次编码),用编辑工具;
|
|
172
|
-
- `package.json` 用 `Set-Content -Encoding utf8` 会写 BOM,`JSON.parse` 直接失败。
|
|
1
|
+
# 结构与开发
|
|
2
|
+
|
|
3
|
+
**[English](../en/architecture.md)** | 简体中文
|
|
4
|
+
|
|
5
|
+
## 形态
|
|
6
|
+
|
|
7
|
+
一个 DSH bundle,两部分:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
index.js # Host 侧:注册 19 个工具、挂设置页的 HTTP 路由
|
|
11
|
+
client.js # Web 侧:设置页(六个标签,无构建步骤,直接是浏览器模块)
|
|
12
|
+
lib/*.js # Host 侧全部逻辑(41 个模块)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
零运行时依赖:只用 Node 内置的 `fetch` / `fs` / `path`。没有 TypeScript、没有打包器、没有 `node_modules`。
|
|
16
|
+
|
|
17
|
+
`cordis.patch.yml` 声明 bundle 与默认配置(`configFile`、`timeoutMs`、`snapshotKeep`、`environments`);
|
|
18
|
+
`package.json` 的 `dsh.bundle.patch` 指向它,`dsh.client` 声明 Web 侧入口。
|
|
19
|
+
|
|
20
|
+
## 模块分工
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
lib/
|
|
24
|
+
client.js Dataverse Web API 客户端:拼 URL、分页、重试、权限闸、令牌
|
|
25
|
+
auth.js Entra ID 客户端凭据流;令牌按凭据指纹缓存
|
|
26
|
+
config.js 配置加载/归一化;configFile 回退链;密钥解析
|
|
27
|
+
resources.js 19 类元数据资源的查询定义与字段映射
|
|
28
|
+
runtime.js makeClient(env, config)、组织信息、版本
|
|
29
|
+
snapshot.js 快照写入、manifest、快照目录
|
|
30
|
+
retention.js 保留策略、磁盘统计、清理、压缩
|
|
31
|
+
write-policy.js 能力表(CAPABILITIES)与按环境的授权判定
|
|
32
|
+
write-actions.js 全部写操作的 planner 与 executor(唯一改环境的代码)
|
|
33
|
+
write-store.js 计划与变更记录的读写、计划状态机
|
|
34
|
+
preflight.js 执行前空闲检查(msdyn_solutionhistories)
|
|
35
|
+
settings-store.js 设置页的数据层(环境、权限、常规参数、GitLab)
|
|
36
|
+
settings-http.js 设置页路由(认证 channel 上的精确路径)
|
|
37
|
+
contract.js 契约事实读取、比对、渲染
|
|
38
|
+
migrate.js 迁移评估(逐组件查层)
|
|
39
|
+
code-check.js MR 检查(JS/TS)
|
|
40
|
+
csharp-check.js MR 检查(C#)
|
|
41
|
+
refs.js 反向索引(表单/视图/流程/步骤)
|
|
42
|
+
health.js 环境健康
|
|
43
|
+
gitlab.js GitLab API(MR diff、连通性)
|
|
44
|
+
waivers.js 契约豁免
|
|
45
|
+
contract-history.js 漂移趋势
|
|
46
|
+
data-migrate.js 业务记录跨环境迁移(计划、lookup 解析、写入、run 记录)
|
|
47
|
+
tool-*.js 19 个工具,一个文件一个工具(或一组相关的)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 数据流
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
agent 调 d365_* ──> tool-*.js ──> lib 能力模块 ──> client.js ──> Dataverse Web API
|
|
54
|
+
│ │
|
|
55
|
+
└────────── 摘要 + 文件路径 <───────────────┘
|
|
56
|
+
│
|
|
57
|
+
.d365/{snapshots,diffs,reports,query,changes}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
设计原则:
|
|
61
|
+
|
|
62
|
+
1. **工具返回摘要与路径,正文落文件**——对话里不塞几十 MB 的 JSON;
|
|
63
|
+
2. **只读优先**——只有 `d365_write execute` 与 `d365_data_write` 会改环境,且必须人批准;
|
|
64
|
+
3. **判定收在一处**——能看什么/能改什么都在 `lib/client.js` 的请求入口判定,新工具无法绕过;
|
|
65
|
+
4. **实时优于快照**——漂移、迁移、健康这类「现在怎么样」的问题读实时;快照只用于跨时间/跨环境比较;
|
|
66
|
+
5. **规则是数据**——契约规则、保留策略、能力表都是表驱动,加规则不改流程。
|
|
67
|
+
|
|
68
|
+
## 一次请求的路径
|
|
69
|
+
|
|
70
|
+
以 `d365_metadata resource=attributes` 为例:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
tool-metadata.js
|
|
74
|
+
→ pickEnvironments(args) 选环境
|
|
75
|
+
→ loadConfig(state.rawConfig) 读配置(每个工具入口都做)
|
|
76
|
+
→ makeClient(env, config) 建客户端
|
|
77
|
+
→ resources.js 的查询定义 拼 path/params
|
|
78
|
+
→ client.getPagedWithSelectFallback 分页 + 列名降级
|
|
79
|
+
→ 权限闸(元数据实体集 → metadata.read;其余 GET → data.read;写 → data.write)
|
|
80
|
+
→ getAccessToken(按凭据指纹缓存)
|
|
81
|
+
→ fetch,429/5xx 按方法决定是否重试
|
|
82
|
+
→ 跨环境对齐 + 摘要
|
|
83
|
+
→ 超过 12 行写 .d365/query/*.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 加一个工具
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
// lib/tool-demo.js
|
|
90
|
+
import { pickEnvironments } from './tool-metadata.js'; // 或自己实现
|
|
91
|
+
|
|
92
|
+
export default function registerDemo(ctx, state) {
|
|
93
|
+
const disposer = ctx.tools.register({
|
|
94
|
+
name: 'd365_demo',
|
|
95
|
+
description: '一句话说明它回答什么问题。',
|
|
96
|
+
parameters: { // 必须是标准 JSON Schema
|
|
97
|
+
type: 'object',
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
required: ['entity'],
|
|
100
|
+
properties: {
|
|
101
|
+
entity: { type: 'string', description: '实体逻辑名' },
|
|
102
|
+
environment: { type: 'string', description: '环境 key' },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
output: {
|
|
106
|
+
schema: { // 同样必须是标准 JSON Schema
|
|
107
|
+
type: 'object',
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
required: ['text'],
|
|
110
|
+
properties: { text: { type: 'string' } },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
async execute(args, exec) {
|
|
114
|
+
const config = await loadConfig(state.rawConfig); // 注意:rawConfig
|
|
115
|
+
...
|
|
116
|
+
return { text };
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
ctx.effect(() => disposer);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
然后 `index.js` 里 import、调用 `registerDemo(ctx, state)`、把 disposer 加进列表。
|
|
124
|
+
|
|
125
|
+
DSH 的硬性要求(都踩过):
|
|
126
|
+
|
|
127
|
+
| 要求 | 症状 |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `parameters` 与 `output.schema` 都是标准 JSON Schema(根节点 `type: 'object'`,`required` 是数组) | `schema must be a JSON Schema of 'type: "object"', got 'type: null'` |
|
|
130
|
+
| 输出严格符合 `output.schema`,不能多字段、不能 `type: ['string','null']` | `value.x is not a declared property` / `type arrays are not supported` |
|
|
131
|
+
| 配置从 `state.rawConfig` 取(`index.js` 传的是 `{ rawConfig }`) | 环境莫名不存在、列表为空 |
|
|
132
|
+
| Host 侧改完要重载插件 | 工具行为还是旧的 |
|
|
133
|
+
|
|
134
|
+
## 客户端(设置页)
|
|
135
|
+
|
|
136
|
+
`client.js` 是一个浏览器模块:`window.__ModuleLoader__.load({ id: 'dsh-d365-kit', factory })`。
|
|
137
|
+
|
|
138
|
+
- 六个标签的面板**同时挂载**,用 `display:none` 隐藏非活动页——切标签不丢编辑状态;
|
|
139
|
+
- 文案内置中英字典,`t()` 每次按 `localeService.getLocale().active` 现算,不依赖 `locale.register` 成功;
|
|
140
|
+
- 语言切换通过订阅 locale 变化触发重渲染;
|
|
141
|
+
- 所有写操作带 `x-d365-meta-write: 1`(Host 侧据此拒绝非设置页请求)。
|
|
142
|
+
|
|
143
|
+
**模块 id 必须与包名一致**,否则加载器会把它当两个模块执行两次,报 `duplicate factory registration` 并让整个设置页消失。
|
|
144
|
+
|
|
145
|
+
## 本地验证
|
|
146
|
+
|
|
147
|
+
没有环境也能测大部分逻辑:stub 掉 `globalThis.fetch` 与 `ctx.tools`,直接调模块函数。
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
globalThis.fetch = async (url) => new Response(JSON.stringify({ value: [] }), { status: 200 });
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
注意:`new Response('', { status: 204 })` 在 Node 里会抛 `Invalid response status code 204`,204 要用 `null` body。
|
|
154
|
+
|
|
155
|
+
真实环境验证的最小集:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
d365_envs check=true
|
|
159
|
+
d365_metadata resource=entities top=2
|
|
160
|
+
d365_layers component=account componentType=Entity
|
|
161
|
+
d365_health environments=["<env>"]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 已知的坑(写给下一个改代码的人)
|
|
165
|
+
|
|
166
|
+
- 元数据端点不接受 `$top` / `$orderby`,客户端有渐进降级;写新查询时优先用 `getPagedWithSelectFallback`;
|
|
167
|
+
- `getPagedWithSelectFallback` 返回 `{ page, selectDropped, ... }`,行在 `result.page.records`;
|
|
168
|
+
- `odataString()` 只转义引号、**不包引号**,过滤器里要自己写 `'...'`;
|
|
169
|
+
- `client.getOne()` 会附加 `$top=1`,对 `solutions` / `solutioncomponents` 会 400,改用 `requestJson` 手动翻页;
|
|
170
|
+
- 层表、`msdyn_solutionhistories` 这类虚拟/审计表的行为与普通表不同,先小样本验证;
|
|
171
|
+
- 含非 ASCII 的文件不要用 PowerShell 5.1 的 `Get-Content | Set-Content` 改写(会按 ANSI 二次编码),用编辑工具;
|
|
172
|
+
- `package.json` 用 `Set-Content -Encoding utf8` 会写 BOM,`JSON.parse` 直接失败。
|