dsh-d365-kit 0.0.0-stage → 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +318 -0
  2. package/CHANGELOG.zh-CN.md +317 -0
  3. package/INSTALL.md +94 -0
  4. package/INSTALL.zh-CN.md +93 -0
  5. package/LICENSE +21 -0
  6. package/README.md +335 -2
  7. package/README.zh-CN.md +336 -0
  8. package/client.js +1818 -0
  9. package/cordis.patch.yml +44 -0
  10. package/docs/en/architecture.md +172 -0
  11. package/docs/en/checks.md +184 -0
  12. package/docs/en/installation.md +151 -0
  13. package/docs/en/permissions.md +161 -0
  14. package/docs/en/scenarios.md +202 -0
  15. package/docs/en/tools.md +665 -0
  16. package/docs/en/troubleshooting.md +131 -0
  17. package/docs/zh/architecture.md +172 -0
  18. package/docs/zh/checks.md +184 -0
  19. package/docs/zh/installation.md +151 -0
  20. package/docs/zh/permissions.md +161 -0
  21. package/docs/zh/scenarios.md +202 -0
  22. package/docs/zh/tools.md +665 -0
  23. package/docs/zh/troubleshooting.md +131 -0
  24. package/examples/prompts.en.md +206 -0
  25. package/examples/prompts.zh-CN.md +206 -0
  26. package/examples/waivers.example.json +20 -0
  27. package/icon.svg +6 -0
  28. package/index.js +142 -0
  29. package/lib/auth.js +181 -0
  30. package/lib/client.js +496 -0
  31. package/lib/code-check.js +549 -0
  32. package/lib/config.js +323 -0
  33. package/lib/contract-history.js +49 -0
  34. package/lib/contract.js +583 -0
  35. package/lib/csharp-check.js +493 -0
  36. package/lib/data-migrate.js +1318 -0
  37. package/lib/data-policy.js +78 -0
  38. package/lib/error-codes.js +80 -0
  39. package/lib/errors.js +81 -0
  40. package/lib/gitlab.js +161 -0
  41. package/lib/health.js +205 -0
  42. package/lib/migrate.js +732 -0
  43. package/lib/preflight.js +116 -0
  44. package/lib/refs.js +191 -0
  45. package/lib/resources.js +320 -0
  46. package/lib/retention-defaults.js +17 -0
  47. package/lib/retention.js +384 -0
  48. package/lib/runtime.js +232 -0
  49. package/lib/settings-http.js +512 -0
  50. package/lib/settings-store.js +371 -0
  51. package/lib/skill.js +76 -0
  52. package/lib/snapshot.js +149 -0
  53. package/lib/tool-code-check.js +133 -0
  54. package/lib/tool-configure.js +169 -0
  55. package/lib/tool-contract.js +376 -0
  56. package/lib/tool-data-sync.js +559 -0
  57. package/lib/tool-data-write.js +203 -0
  58. package/lib/tool-data.js +282 -0
  59. package/lib/tool-diff.js +484 -0
  60. package/lib/tool-environments.js +106 -0
  61. package/lib/tool-flows.js +540 -0
  62. package/lib/tool-health.js +127 -0
  63. package/lib/tool-invoke.js +284 -0
  64. package/lib/tool-layers.js +497 -0
  65. package/lib/tool-metadata.js +417 -0
  66. package/lib/tool-migrate.js +102 -0
  67. package/lib/tool-refs.js +206 -0
  68. package/lib/tool-reports.js +299 -0
  69. package/lib/tool-sync.js +430 -0
  70. package/lib/tool-trace.js +420 -0
  71. package/lib/tool-write.js +563 -0
  72. package/lib/waivers.js +83 -0
  73. package/lib/write-actions.js +1374 -0
  74. package/lib/write-policy.js +199 -0
  75. package/lib/write-store.js +167 -0
  76. package/locale/en.json +68 -0
  77. package/locale/zh.json +68 -0
  78. package/package.json +59 -4
  79. package/skills/d365-troubleshooting/SKILL.md +115 -0
@@ -0,0 +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).
@@ -0,0 +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` 直接失败。
@@ -0,0 +1,184 @@
1
+ # 检查规则
2
+
3
+ **[English](../en/checks.md)** | 简体中文
4
+
5
+ 三类检查工具回答不同问题:
6
+
7
+ | 工具 | 问题 | 数据来源 |
8
+ | --- | --- | --- |
9
+ | `d365_migrate` | 把这个解决方案导进目标环境会发生什么 | 实时读,逐组件查层 |
10
+ | `d365_contract` | 上游的注册与引用,下游是否还在履行 | 实时读(漂移要看当下) |
11
+ | `d365_code_check` | 这次 MR 的代码改动在目标环境跑得起来吗 | GitLab MR + 实时读环境 |
12
+
13
+ 共同约定:**高置信直接阻断,中置信降为警告,静态判不了的进人工确认**——宁可让人多看一眼,也不要用一条误报把工具变成噪声。
14
+
15
+ ---
16
+
17
+ ## 迁移评估(d365_migrate assess)
18
+
19
+ 逐组件比对,产生这些发现:
20
+
21
+ | 规则 | 判据 | 级别 |
22
+ | --- | --- | --- |
23
+ | `component-missing` | 组件在目标环境不存在 | info |
24
+ | `solution-absent` | 目标没有这个解决方案(首次导入) | warning |
25
+ | `version-not-newer` | 目标版本不低于源版本(导入不会覆盖成新的) | warning |
26
+ | `managed-mismatch` | 源与目标的受管状态不一致 | warning |
27
+ | `source-unmanaged` | 源是未受管,导入后形成无法卸载清除的层 | warning |
28
+ | `option-values-lost` | 目标有、源没有的选项值——导入后会被删掉 | **critical** |
29
+ | `option-label-drift` | 同一值标签不同 | warning |
30
+ | `shadowed` | 目标该组件最上层是别的解决方案,导入后其定义失效 | 汇总列出 |
31
+
32
+ ### 组件类型与层
33
+
34
+ 层表能按名称查的类型只有六种:
35
+
36
+ | `msdyn_solutioncomponentname` | componenttype |
37
+ | --- | --- |
38
+ | `Entity` | 1 |
39
+ | `Attribute` | 2 |
40
+ | `OptionSet` | 9 |
41
+ | `EntityRelationship` | 10 |
42
+ | `Workflow` | 29 |
43
+ | `WebResource` | 61 |
44
+
45
+ 其余类型(36 报表、50 图表/安全类、70 应用类…)在层表里查不到对应名称,只做**存在性检查**,报告里标「未评估」。
46
+
47
+ 跨环境识别的键来自层记录的 `msdyn_componentjson`:Entity → `logicalname`,Attribute → `logicalname` + 父表,OptionSet → `name`,EntityRelationship → `schemaname`,Workflow → `name` + `category`,WebResource → `name`。
48
+
49
+ ### 为什么慢
50
+
51
+ `msdyn_componentlayer` 是虚拟实体,只能按「组件 id + 组件类型」逐个查,无法按解决方案批量取;单组件约 2 秒。所以有 `maxComponents`(默认 150)与 `concurrency`(默认 4,上限 8),`full: true` 才评估全部。
52
+
53
+ ### 读报告的顺序
54
+
55
+ 1. 先看**严重**:有没有丢选项值;
56
+ 2. 再看**遮蔽**:导入后谁的定义会失效,是不是自己家的受管 patch;
57
+ 3. 最后看**缺失与版本**:目标缺组件通常意味着发布链路上少了一步;
58
+ 4. 结论落地:能并入受管解决方案随升级走,就不要单独发一个未受管的小解决方案——层是按导入时间叠的,下一次谁的 patch 进来就会把它盖回去。
59
+
60
+ ---
61
+
62
+ ## 契约漂移(d365_contract)
63
+
64
+ 把上游的插件注册与可视化组件引用当成**契约**,检查下游是否还满足。
65
+
66
+ | 规则 | 判据 | 级别 |
67
+ | --- | --- | --- |
68
+ | `assembly-missing` | 上游有、下游没有该程序集 | 阻断 |
69
+ | `assembly-version-older` | 下游版本低于上游 | 警告 |
70
+ | `plugin-type-missing` | 上游注册的类型,下游没有 | 阻断 |
71
+ | `plugin-step-missing` | 类型在,但缺「消息 + stage + mode」组合的步骤 | 阻断 |
72
+ | `form-library-missing` | 下游表单引用的 Web 资源不存在 | 阻断 |
73
+ | `sitemap-missing` | 上游的自定义站点地图在下游没有 | 警告 |
74
+ | `handler-missing` | 表单 Handler 指定的函数在项目脚本里找不到 | 警告 |
75
+ | `option-value-missing` | 下游缺上游的选项值(或多出上游没有的值) | 警告 |
76
+
77
+ 细节:
78
+
79
+ - 默认只比**未受管**程序集;受管程序集来自微软或受管解决方案,本地改不了;
80
+ - 步骤按「类型 + 消息 + stage + mode」比对:async 与 sync 是两条不同的契约;
81
+ - 选项值比对需要显式传 `optionSets`(逐个读 `GlobalOptionSetDefinitions`),不传就不比;
82
+ - 下游比上游**多**的内容不报阻断,只在选项集多值时提示「确认是否已回灌」。
83
+
84
+ ### Handler 检查
85
+
86
+ `handlers: true` 时下载表单引用的项目脚本,核对 `functionName` 是否存在。
87
+
88
+ - 只判定**项目脚本**。前缀默认从源环境的未受管解决方案推断——取占多数的那个发布者前缀,再加 `new_`——所以换一个组织、换一个前缀都不用改配置;要精确指定就在配置里写 `handlerPrefixes`。合作方与示例解决方案故意排除:它们的包是压缩过的,内部名字被混淆,全算进来每个 Handler 都会像缺失(某真实环境上:94 条而不是 14 条);
89
+ - 先查 Handler 自己的库,找不到再扩大到该表单加载的其它项目库(表单级共享作用域是真实存在的:一个库提供函数,另一个库绑定);
90
+ - 平台注入的 Handler(`absoluteurl_onchange`、`Main_Form_onload` 等)来自系统脚本,不参与判定;
91
+ - 逐个下载有上限(400 个脚本/环境),超出的部分按「读不到内容、无法确认」处理,不误报成阻断。
92
+
93
+ 首版按「表单加载的全部脚本库」判定时产生 306 条警告,收敛到「项目库 + 自己的库优先」后是 14 条,全部是读完了确实没有。**这个数字本身就是设计目标:可行动的少量,而不是不可读的大量。**
94
+
95
+ ### 豁免与趋势
96
+
97
+ ```json
98
+ { "action": "waive", "rule": "form-library-missing", "env": "sit",
99
+ "object": "/_static/_common/scripts/CommandbarActions.js", "reason": "该环境不用 SLA 对话框", "days": 30 }
100
+ ```
101
+
102
+ - 写在 `.d365/waivers.json`;`env` 支持 `*`,`object` 支持 `*` 与前缀 `x*`;
103
+ - 必须有过期时间(默认 30 天),过期自动重新出现在报告里;
104
+ - 报告显示「已豁免 N 条」,被豁免的条目不进阻断列表;
105
+ - 每次 drift 追加一条到 `.d365/reports/contract-history.json`(保留 60 条),下次跑同一组 `source → targets` 给出「新增 / 已消失」。
106
+
107
+ ### 为什么读实时数据
108
+
109
+ 插件注册规模:一个环境约 376 个程序集、6400 个类型、5900 个有步骤的类型(约 9.4 万条 step),几十秒读完。用几天前的快照做漂移会得出假结论,所以这里不用快照。
110
+
111
+ ---
112
+
113
+ ## 代码检查(d365_code_check)
114
+
115
+ 读一次 MR,只分析它改到的文件。需要 GitLab 只读令牌(`read_api`),令牌所在用户至少 **Reporter**。
116
+
117
+ ### JS / TS
118
+
119
+ 检查链:改动文件 → 按**文件名**匹配环境里的 Web 资源(路径不一致时按同名候选取最接近的一个,报告里标「推断,待确认」)→ 找出引用该资源的表单 → 逐条比对改动行。
120
+
121
+ | 检查 | 结论 |
122
+ | --- | --- |
123
+ | 新增行读取的字段不在**任何**引用该库的表单上 | 阻断一次 |
124
+ | 字段只在部分表单上(脚本可能不会在这些表单走到该分支) | 警告,列出缺的表单(最多 3 个) |
125
+ | 引用的实体不存在(表单 `datafieldname`、`Xrm.WebApi` 的实体集) | 阻断 |
126
+ | 删除的函数仍被表单 Handler 引用 | 阻断 |
127
+ | 删掉的文件仍被表单引用 | 阻断(没有表单引用时不报) |
128
+ | `columns.get('x')` | 人工确认(视图/子网格列,不在表单字段里是正常的) |
129
+ | 带前缀的字符串下标 `["contoso_x"]` | 人工确认(静态判不出是字段还是普通键) |
130
+ | 新出现硬编码 GUID、选项值、环境地址 | 警告或人工确认 |
131
+ | 配置数据、字段级安全、运行用户权限 | 人工确认(静态判不了) |
132
+
133
+ 取值写法覆盖:`getAttribute`、`getControl`、`attributes.get`、`formContext.data.entity.attributes.get`、`Xrm.Page.getAttribute`、`attributes[...]`、带前缀字符串下标。
134
+ 只靠 `getAttribute` 一条正则会漏掉大量编译产物(实测一个 103 KB 的 TS 编译脚本命中 0 次),所以按写法分成三组:确定是字段、确定是列、需要人判。
135
+
136
+ ### C# / 工程文件
137
+
138
+ 按路径里**最靠内的工程目录**推断程序集名(`.csproj` 里有 `<AssemblyName>` 时以它为准)→ 与环境里的 `pluginassemblies` 做**精确匹配** → 查 `plugintypes`、`sdkmessageprocessingsteps`、`sdkmessageprocessingstepimages`。
139
+
140
+ | 检查 | 结论 |
141
+ | --- | --- |
142
+ | 程序集在目标环境不存在(共享工程如 `.Processes` 属于这种情况) | 警告 + 人工确认该文件最终随哪个程序集发布 |
143
+ | 源码版本与环境程序集版本不一致 | 警告 |
144
+ | `IPlugin` 类在目标环境没有对应插件类型 | 阻断 |
145
+ | 类型存在但没有任何步骤(改了也不会被触发) | 警告 |
146
+ | 代码读 `PreEntityImages["X"]` / `PostEntityImages["X"]`,对应步骤没注册该镜像 | 阻断(Create 步骤不检查 pre-image) |
147
+ | 步骤的 `filteringattributes` 引用该实体上不存在的字段 | 阻断 |
148
+ | 枚举 `contoso_*` 字面量在目标既不是表也不是全局选项集 | 人工确认 |
149
+ | 同一 `message\|stage` 上 ≥2 个步骤 | 警告(rank 重复时执行顺序不确定) |
150
+ | 代码用到 `secureConfig` / 构造函数两个 string 参数 | 人工确认(Web API 不返回 secure configuration 内容) |
151
+ | 沙箱可能禁止的 API(`System.IO`、`System.Net`、`Thread.Sleep`…) | 警告 |
152
+
153
+ 程序集名只做精确匹配,不猜近似的——猜错会把类型/步骤核对到别的程序集上,比不核对更糟。
154
+
155
+ ### 报告结构
156
+
157
+ ```
158
+ 改动文件表
159
+ 每文件影响面(映射到哪个资源/程序集、哪些表单/类型)
160
+ 阻断(必须修)
161
+ 警告(建议修,或登记豁免)
162
+ 人工确认(HC-01…,每条写清为什么机器判不了)
163
+ ```
164
+
165
+ ### 已知边界
166
+
167
+ - 只分析 MR 的增删行,不读仓库其它文件(不解析 import、不跟踪调用链);
168
+ - 路径 → Web 资源名靠文件名推断,仓库路径与资源名不一致时会标「推断」;
169
+ - C# 的过滤属性、镜像、类型只看注册信息;secure/unsecure 配置内容与 rank 之外的隐式顺序仍需人工确认;
170
+ - 不判断运行用户权限与实际数据形态。
171
+
172
+ ---
173
+
174
+ ## 三类检查的分工
175
+
176
+ ```
177
+ 代码改动 ──d365_code_check──> 这次改动能不能上线(单次、面向 MR)
178
+ 上游 vs 下游 ──d365_contract──> 环境之间的契约有没有破(持续、面向版本)
179
+ 解决方案 ──d365_migrate──> 这次导入会不会压掉别人(单次、面向发布)
180
+ 谁覆盖了它 ──d365_layers──> 为什么这个环境是现在这个样子
181
+ 谁在用它 ──d365_refs──> 改/删它的影响面
182
+ ```
183
+
184
+ 它们不互相替代:`d365_layers` 告诉你「谁覆盖」,`d365_refs` 告诉你「谁引用」,`d365_code_check` 告诉你「这次改的行有没有落在不存在的东西上」。
@@ -0,0 +1,151 @@
1
+ # 安装与配置
2
+
3
+ **[English](../en/installation.md)** | 简体中文
4
+
5
+ ## 前置
6
+
7
+ | 项 | 要求 |
8
+ | --- | --- |
9
+ | DSH | 桌面版,能打开 Web GUI(设置页是 Web 客户端插件) |
10
+ | 网络 | 能访问环境域名(`*.crm*.dynamics.com` 或对应区域)与登录端点 |
11
+ | 应用用户 | Entra ID 里的应用注册 + 在 Dataverse 里建的应用用户,拿到 `ClientId` / `ClientSecret` / `TenantId` |
12
+ | 权限 | 读元数据、读解决方案(`prvReadSolution`)、读自定义 API(`prvReadCustomAPI`)、读层表(`prvReadmsdyn_componentlayer`) |
13
+
14
+ 只读应用用户就够用;写入能力用到的 `CreateAttribute` 等消息,需要该用户在环境里有对应权限(系统定制员或自定义角色里勾相应权限)。
15
+
16
+ > 应用用户在一个环境里建好后,要在**每个**目标环境各建一次(同一个 ClientId 可以复用)。
17
+
18
+ ## 安装
19
+
20
+ ### 方式一:从 npm 安装(最短)
21
+
22
+ ```bash
23
+ plugin_manager install_bundle target="dsh-d365-kit@0.34.3"
24
+ ```
25
+
26
+ 前提是包已经发到 pnpm 配置的 registry——公网 npm 或内网镜像都行。包名没有 scope,不需要配 scope 映射。要装指定版本就写 `dsh-d365-kit@<版本>`。
27
+
28
+ > **只发 GitHub 不等于发了 npm。** 仓库可以直接用第二种 `git+https://...` 装,`npm install github:<user>/<repo>#<tag>` 也能装;但 `install_bundle target="dsh-d365-kit"` 只有在包进了 registry 之后才解析得到。仓库里带 `.github/workflows/publish.yml`:配好 `NPM_TRUSTED_PUBLISHING` 变量或 `NPM_TOKEN` secret 后,每次推 `main` 且版本号是新的,就会自动发布。
29
+
30
+ ### 方式二:从 Git 安装(不需要 registry)
31
+
32
+ ```bash
33
+ plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.3"
34
+ ```
35
+
36
+ DSH 用 pnpm 从 git 拉包。装完确认:
37
+
38
+ ```
39
+ plugin_manager list_plugins
40
+ ```
41
+
42
+ `include:dsh-d365-kit` 存在且 `enabled=true`、`fiberPhase=active` 即成功。
43
+
44
+ ### 方式三:从本地目录安装
45
+
46
+ 把整个仓库目录放到本机任意位置:
47
+
48
+ ```bash
49
+ plugin_manager install_bundle target="D:\path\to\dsh-d365-kit"
50
+ ```
51
+
52
+ 这是 `link:` 安装:改代码立刻生效(但 Host 侧仍需重载),升级要自己 `git pull`。
53
+
54
+ ### 方式四:zip
55
+
56
+ 解压后按方式三装。
57
+
58
+ ### 装完必须重启一次
59
+
60
+ Host 侧工具在安装时就能用;**设置页**属于 Web 客户端插件,DSH 对客户端插件的模块图有缓存,必须重启 DSH 才会把页面加载进来。
61
+
62
+ 重启后打开 设置,应看到「D365 工具箱」。看不到时先跑 `d365_envs` 确认 Host 侧已加载,再看 `plugin_manager list_plugins` 里的条目名。
63
+
64
+ ## 首次配置
65
+
66
+ 1. 设置 → D365 工具箱 → **环境** → 添加环境:
67
+
68
+ | 字段 | 说明 |
69
+ | --- | --- |
70
+ | key | 工具里用的短名,例如 `dev`、`sit`。**一旦写入计划就会记进审计,建议起稳定的名字** |
71
+ | URL | 环境地址,例如 `https://contoso-dev.crm5.dynamics.com` |
72
+ | 云 | `public` / `usgovernment` / `dod` / `china`,决定登录端点 |
73
+ | 租户 ID | Entra 租户 GUID |
74
+ | ClientId | 应用注册的 Application (client) ID |
75
+ | 客户端密钥 | 应用注册里生成的 secret 值 |
76
+
77
+ 2. 点「测试连接」,返回组织名与版本号说明凭据正确。
78
+ 3. **保存**。密钥写进 `d365.secrets.json`,其余进 `d365.config.json`。
79
+ 4. **权限**标签按环境分块列出能力:「读取元数据」默认开启(关掉后连表、字段、视图都查不到);要读业务记录再勾「读取业务数据」(默认关闭)。
80
+ 5. 需要写入时,勾对应能力,并填「变更默认解决方案」(一个**已存在的未受管**解决方案唯一名)。
81
+
82
+ ## 配置存在哪
83
+
84
+ 环境清单与工具参数存在 `configFile`:
85
+
86
+ ```
87
+ <工作区>/d365.config.json # 环境清单、默认环境、工具参数、权限
88
+ <工作区>/d365.secrets.json # 只放 clientSecret 与 gitlabToken(明文)
89
+ ```
90
+
91
+ `configFile` 的解析顺序:
92
+
93
+ 1. `cordis.patch.yml` 里的 `configFile`(绝对路径,安装时的机器相关配置)
94
+ 2. 环境变量 `D365_CONFIG_FILE`
95
+ 3. `<USERPROFILE>\Documents\deepseek-harness\default-workspace\d365.config.json`(该目录存在时)
96
+ 4. `<当前工作目录>\d365.config.json`
97
+
98
+ 仓库里的 `cordis.patch.yml` 不带 `configFile`,所以默认落到第 3 条;工作区在别处时用环境变量或 patch 指定。
99
+ `d365.secrets.json` 始终与 `configFile` 同目录。
100
+
101
+ **这两个文件不要提交到 Git**:仓库的 `.gitignore` 已经挡住它们与 `.d365/`。
102
+
103
+ ## 凭据解析顺序
104
+
105
+ 每个值(`tenantId` / `clientId` / `clientSecret`)按顺序取:
106
+
107
+ 1. 环境配置里的内联值;
108
+ 2. 设置页写进 `d365.secrets.json` 的 `clientSecret`(覆盖层);
109
+ 3. 该环境配置里 `<kind>Env` 指定的环境变量名,如 `"clientSecretEnv": "D365_DEV_SECRET"`;
110
+ 4. 派生名 `D365_<ENV>_<KIND>`,如 `D365_dev_CLIENT_SECRET`(key 原样,含连字符);
111
+ 5. 共享名 `D365_TENANT_ID` / `D365_CLIENT_ID` / `D365_CLIENT_SECRET`。
112
+
113
+ 生产环境更建议用环境变量,本机调试用设置页即可。
114
+
115
+ 令牌在进程内存里缓存,按「URL + 租户 + ClientId + 密钥」的指纹区分——换了凭据会立即重取,不用重启。
116
+
117
+ ## 验证
118
+
119
+ ```
120
+ d365_envs check=true
121
+ d365_metadata resource=entities top=3
122
+ ```
123
+
124
+ 两次都有内容就通了。第一次 `d365_sync` 会拉全量元数据(真实环境约 125 MB、三分钟),不想占盘可以跳过,用 `d365_metadata` 按需查。
125
+
126
+ ## 升级
127
+
128
+ ```bash
129
+ # 从 Git 安装的
130
+ plugin_manager remove_bundle target=dsh-d365-kit
131
+ plugin_manager install_bundle target="git+https://<your-git>/<group>/dsh-d365-kit.git#v0.34.3"
132
+ ```
133
+
134
+ 改版本号后必须**先 remove 再 install**:link/pnpm 认为包已存在时会返回 `already-installed`,不会替换代码。
135
+
136
+ 升级后重启 DSH(设置页代码要重新进模块图)。
137
+
138
+ ## 卸载
139
+
140
+ ```bash
141
+ plugin_manager remove_bundle target=dsh-d365-kit
142
+ ```
143
+
144
+ 本机数据(`d365.config.json`、`d365.secrets.json`、`.d365/`)不会删,需要时手动清理。
145
+
146
+ ## 多人共用
147
+
148
+ - 每个人都装一份,各自填自己的 `d365.config.json` 与密钥;
149
+ - **权限是按机器授权的**:别人机器上的勾选不影响你,反之亦然;
150
+ - 报告与快照都在各自工作区,不共享;
151
+ - 想让全组用同一套环境定义,可把 `d365.config.json` 的 `environments` 放进内部文档,但密钥仍各自填。