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.
Files changed (75) hide show
  1. package/CHANGELOG.md +318 -308
  2. package/CHANGELOG.zh-CN.md +317 -307
  3. package/INSTALL.md +94 -94
  4. package/INSTALL.zh-CN.md +93 -93
  5. package/LICENSE +21 -21
  6. package/README.md +336 -327
  7. package/README.zh-CN.md +336 -327
  8. package/client.js +1818 -1818
  9. package/cordis.patch.yml +44 -44
  10. package/docs/en/architecture.md +172 -172
  11. package/docs/en/checks.md +184 -184
  12. package/docs/en/installation.md +151 -151
  13. package/docs/en/permissions.md +161 -161
  14. package/docs/en/scenarios.md +202 -202
  15. package/docs/en/tools.md +665 -665
  16. package/docs/en/troubleshooting.md +131 -131
  17. package/docs/zh/architecture.md +172 -172
  18. package/docs/zh/checks.md +184 -184
  19. package/docs/zh/installation.md +151 -151
  20. package/docs/zh/permissions.md +161 -161
  21. package/docs/zh/scenarios.md +202 -202
  22. package/docs/zh/tools.md +665 -665
  23. package/docs/zh/troubleshooting.md +131 -131
  24. package/examples/prompts.en.md +206 -206
  25. package/examples/prompts.zh-CN.md +206 -206
  26. package/examples/waivers.example.json +20 -20
  27. package/icon.svg +6 -6
  28. package/index.js +139 -139
  29. package/lib/auth.js +181 -181
  30. package/lib/client.js +496 -496
  31. package/lib/code-check.js +549 -549
  32. package/lib/config.js +323 -323
  33. package/lib/contract-history.js +49 -49
  34. package/lib/contract.js +583 -583
  35. package/lib/csharp-check.js +493 -493
  36. package/lib/data-migrate.js +1318 -1318
  37. package/lib/data-policy.js +78 -78
  38. package/lib/error-codes.js +80 -80
  39. package/lib/gitlab.js +161 -161
  40. package/lib/health.js +205 -205
  41. package/lib/migrate.js +732 -732
  42. package/lib/preflight.js +116 -116
  43. package/lib/refs.js +191 -191
  44. package/lib/resources.js +320 -320
  45. package/lib/retention-defaults.js +17 -17
  46. package/lib/retention.js +384 -384
  47. package/lib/runtime.js +232 -232
  48. package/lib/settings-http.js +512 -512
  49. package/lib/settings-store.js +371 -371
  50. package/lib/snapshot.js +149 -149
  51. package/lib/tool-code-check.js +133 -133
  52. package/lib/tool-configure.js +169 -169
  53. package/lib/tool-contract.js +376 -376
  54. package/lib/tool-data-sync.js +559 -559
  55. package/lib/tool-data-write.js +203 -203
  56. package/lib/tool-data.js +282 -282
  57. package/lib/tool-diff.js +484 -484
  58. package/lib/tool-environments.js +106 -106
  59. package/lib/tool-flows.js +540 -540
  60. package/lib/tool-health.js +127 -127
  61. package/lib/tool-layers.js +497 -497
  62. package/lib/tool-metadata.js +417 -417
  63. package/lib/tool-migrate.js +102 -102
  64. package/lib/tool-refs.js +206 -206
  65. package/lib/tool-reports.js +299 -299
  66. package/lib/tool-sync.js +430 -430
  67. package/lib/tool-write.js +563 -563
  68. package/lib/waivers.js +83 -83
  69. package/lib/write-actions.js +1374 -1374
  70. package/lib/write-policy.js +199 -199
  71. package/lib/write-store.js +167 -167
  72. package/locale/en.json +68 -68
  73. package/locale/zh.json +68 -68
  74. package/package.json +2 -2
  75. package/skills/d365-troubleshooting/SKILL.md +115 -115
@@ -1,102 +1,102 @@
1
- /**
2
- * `d365_migrate` — will this solution zip hurt when imported elsewhere?
3
- *
4
- * Importing a solution silently stacks a new layer on every component it
5
- * contains, and stacking order is decided by import time, not by version. This
6
- * tool answers the questions a release engineer actually asks before pressing
7
- * import: does the target have this solution, which solutions' definitions will
8
- * be hidden underneath, and will any option-set values disappear.
9
- */
10
- import { loadConfig } from './config.js'
11
- import { assessMigration } from './migrate.js'
12
-
13
- export const definition = {
14
- name: 'd365_migrate',
15
- description:
16
- '评估把某个解决方案从源环境发布到其它环境的迁移风险:目标是否已有该解决方案、每个组件在目标环境的层会被谁覆盖、'
17
- + '选项集会不会丢值。只读,不会修改任何环境。',
18
- parameters: {
19
- type: 'object',
20
- additionalProperties: false,
21
- required: ['solution'],
22
- properties: {
23
- action: {
24
- type: 'string',
25
- enum: ['assess'],
26
- description: '评估动作,目前只有 assess。默认 assess。',
27
- },
28
- source: {
29
- type: 'string',
30
- description: '源环境 key,例如 dev。省略则用默认环境。',
31
- },
32
- solution: {
33
- type: 'string',
34
- description: '要评估的解决方案唯一名,例如 ContosoBase。',
35
- },
36
- targets: {
37
- type: 'array',
38
- items: { type: 'string' },
39
- description: '目标环境 key 列表,例如 ["sit","uat"]。省略则除源环境外的全部环境。',
40
- },
41
- maxComponents: {
42
- type: 'integer',
43
- description: '最多评估多少个组件,默认 150。单组件层查询约 1.8 秒,并发 4 时约 70 秒。',
44
- },
45
- full: {
46
- type: 'boolean',
47
- description: '忽略 maxComponents,评估全部组件(364 个组件约 3 分钟)。',
48
- },
49
- concurrency: {
50
- type: 'integer',
51
- description: '并发请求数,默认 4,最大 8。调高会更快,也更容易触发服务保护。',
52
- },
53
- },
54
- },
55
- output: {
56
- schema: {
57
- type: 'object',
58
- additionalProperties: false,
59
- required: ['text'],
60
- properties: {
61
- text: { type: 'string' },
62
- },
63
- },
64
- render(_args, value) {
65
- return [{ type: 'text', text: value?.text ?? '' }]
66
- },
67
- },
68
- }
69
-
70
- function renderResult(result) {
71
- const lines = []
72
- lines.push(`迁移评估:${result.solution}(${result.source.label})`)
73
- lines.push(
74
- `组件 ${result.componentCount} 个,本次评估 ${result.evaluated} 个`
75
- + (result.truncated ? `(超过 maxComponents=${result.maxComponents},传 full: true 可全量评估)` : ''),
76
- )
77
- lines.push('')
78
- lines.push('| 目标环境 | 严重 | 警告 | 目标缺失组件 | 会被遮蔽的解决方案 |')
79
- lines.push('| --- | --- | --- | --- | --- |')
80
- for (const report of result.reports) {
81
- lines.push(`| ${report.targetLabel} | ${report.critical} | ${report.warnings} | ${report.missing} | ${report.shadowed} |`)
82
- }
83
- for (const report of result.reports) {
84
- lines.push('')
85
- lines.push(`### ${report.targetLabel}`)
86
- lines.push(report.verdict.summary)
87
- if (report.verdict.recommendation) lines.push(`建议:${report.verdict.recommendation}`)
88
- lines.push(`报告:${report.reportPath}`)
89
- }
90
- return lines.join('\n')
91
- }
92
-
93
- export function register(ctx, state) {
94
- return ctx.tools.register({
95
- ...definition,
96
- async execute(args, exec) {
97
- const config = await loadConfig(state.rawConfig)
98
- const result = await assessMigration(config, args ?? {}, exec?.signal)
99
- return { text: renderResult(result) }
100
- },
101
- })
102
- }
1
+ /**
2
+ * `d365_migrate` — will this solution zip hurt when imported elsewhere?
3
+ *
4
+ * Importing a solution silently stacks a new layer on every component it
5
+ * contains, and stacking order is decided by import time, not by version. This
6
+ * tool answers the questions a release engineer actually asks before pressing
7
+ * import: does the target have this solution, which solutions' definitions will
8
+ * be hidden underneath, and will any option-set values disappear.
9
+ */
10
+ import { loadConfig } from './config.js'
11
+ import { assessMigration } from './migrate.js'
12
+
13
+ export const definition = {
14
+ name: 'd365_migrate',
15
+ description:
16
+ '评估把某个解决方案从源环境发布到其它环境的迁移风险:目标是否已有该解决方案、每个组件在目标环境的层会被谁覆盖、'
17
+ + '选项集会不会丢值。只读,不会修改任何环境。',
18
+ parameters: {
19
+ type: 'object',
20
+ additionalProperties: false,
21
+ required: ['solution'],
22
+ properties: {
23
+ action: {
24
+ type: 'string',
25
+ enum: ['assess'],
26
+ description: '评估动作,目前只有 assess。默认 assess。',
27
+ },
28
+ source: {
29
+ type: 'string',
30
+ description: '源环境 key,例如 dev。省略则用默认环境。',
31
+ },
32
+ solution: {
33
+ type: 'string',
34
+ description: '要评估的解决方案唯一名,例如 ContosoBase。',
35
+ },
36
+ targets: {
37
+ type: 'array',
38
+ items: { type: 'string' },
39
+ description: '目标环境 key 列表,例如 ["sit","uat"]。省略则除源环境外的全部环境。',
40
+ },
41
+ maxComponents: {
42
+ type: 'integer',
43
+ description: '最多评估多少个组件,默认 150。单组件层查询约 1.8 秒,并发 4 时约 70 秒。',
44
+ },
45
+ full: {
46
+ type: 'boolean',
47
+ description: '忽略 maxComponents,评估全部组件(364 个组件约 3 分钟)。',
48
+ },
49
+ concurrency: {
50
+ type: 'integer',
51
+ description: '并发请求数,默认 4,最大 8。调高会更快,也更容易触发服务保护。',
52
+ },
53
+ },
54
+ },
55
+ output: {
56
+ schema: {
57
+ type: 'object',
58
+ additionalProperties: false,
59
+ required: ['text'],
60
+ properties: {
61
+ text: { type: 'string' },
62
+ },
63
+ },
64
+ render(_args, value) {
65
+ return [{ type: 'text', text: value?.text ?? '' }]
66
+ },
67
+ },
68
+ }
69
+
70
+ function renderResult(result) {
71
+ const lines = []
72
+ lines.push(`迁移评估:${result.solution}(${result.source.label})`)
73
+ lines.push(
74
+ `组件 ${result.componentCount} 个,本次评估 ${result.evaluated} 个`
75
+ + (result.truncated ? `(超过 maxComponents=${result.maxComponents},传 full: true 可全量评估)` : ''),
76
+ )
77
+ lines.push('')
78
+ lines.push('| 目标环境 | 严重 | 警告 | 目标缺失组件 | 会被遮蔽的解决方案 |')
79
+ lines.push('| --- | --- | --- | --- | --- |')
80
+ for (const report of result.reports) {
81
+ lines.push(`| ${report.targetLabel} | ${report.critical} | ${report.warnings} | ${report.missing} | ${report.shadowed} |`)
82
+ }
83
+ for (const report of result.reports) {
84
+ lines.push('')
85
+ lines.push(`### ${report.targetLabel}`)
86
+ lines.push(report.verdict.summary)
87
+ if (report.verdict.recommendation) lines.push(`建议:${report.verdict.recommendation}`)
88
+ lines.push(`报告:${report.reportPath}`)
89
+ }
90
+ return lines.join('\n')
91
+ }
92
+
93
+ export function register(ctx, state) {
94
+ return ctx.tools.register({
95
+ ...definition,
96
+ async execute(args, exec) {
97
+ const config = await loadConfig(state.rawConfig)
98
+ const result = await assessMigration(config, args ?? {}, exec?.signal)
99
+ return { text: renderResult(result) }
100
+ },
101
+ })
102
+ }
package/lib/tool-refs.js CHANGED
@@ -1,206 +1,206 @@
1
- /**
2
- * `d365_refs` — the reverse question: who uses this?
3
- *
4
- * `d365_metadata` answers "does it exist"; `d365_layers` answers "who overrides
5
- * it"; this answers "what breaks if I remove or rename it". Every branch reads
6
- * stored definitions (formxml, layoutxml/fetchxml, workflow clientdata) and
7
- * reports exact matches, so the answer is evidence rather than a guess.
8
- */
9
- import { loadConfig } from './config.js'
10
- import { makeClient, pickEnvironments, writeArtifact } from './runtime.js'
11
- import { D365Error } from './errors.js'
12
- import {
13
- findFieldAutomations,
14
- findFieldForms,
15
- findFieldViews,
16
- findPluginUsage,
17
- findResourceForms,
18
- } from './refs.js'
19
-
20
- const KINDS = ['field', 'web_resource', 'plugin_assembly']
21
-
22
- export const definition = {
23
- name: 'd365_refs',
24
- description:
25
- '反查引用:某个字段被哪些表单/视图/流程使用,某个 Web 资源被哪些表单加载,某个插件程序集有哪些注册类型与步骤。'
26
- + '用于判断"改这个/删这个会不会坏",与 d365_layers(谁覆盖)互补。可一次查多个环境。',
27
- parameters: {
28
- type: 'object',
29
- additionalProperties: false,
30
- required: ['kind'],
31
- properties: {
32
- kind: {
33
- type: 'string',
34
- enum: KINDS,
35
- description: 'field:字段引用;web_resource:表单脚本引用;plugin_assembly:插件注册情况。',
36
- },
37
- entity: {
38
- type: 'string',
39
- description: 'kind=field 时必填:字段所属实体逻辑名,例如 account。',
40
- },
41
- attribute: {
42
- type: 'string',
43
- description: 'kind=field 时必填:字段逻辑名,例如 contoso_customertier。',
44
- },
45
- name: {
46
- type: 'string',
47
- description: 'kind=web_resource 时填 Web 资源名称(如 contoso_/scripts/account_form.js);kind=plugin_assembly 时填程序集名(如 Contoso.Customizations)。',
48
- },
49
- environment: {
50
- type: 'string',
51
- description: '环境 key,例如 dev。省略则用默认环境。',
52
- },
53
- environments: {
54
- type: 'array',
55
- items: { type: 'string' },
56
- description: '一次查多个环境,例如 ["dev","sit"]。',
57
- },
58
- allEnvironments: {
59
- type: 'boolean',
60
- description: '对全部已配置环境执行同一查询。',
61
- },
62
- },
63
- },
64
- output: {
65
- schema: {
66
- type: 'object',
67
- additionalProperties: false,
68
- required: ['text'],
69
- properties: {
70
- text: { type: 'string' },
71
- },
72
- },
73
- render(_args, value) {
74
- return [{ type: 'text', text: value?.text ?? '' }]
75
- },
76
- },
77
- }
78
-
79
- /** Render a field reference report for one environment. */
80
- function renderField(env, entity, attribute, result) {
81
- const lines = [`字段 ${entity}.${attribute} 的引用(${env.key})`]
82
- const total = result.forms.length + result.views.length + result.automations.length
83
-
84
- lines.push('', `表单:${result.forms.length} 张`)
85
- for (const form of result.forms.slice(0, 10)) {
86
- lines.push(`- ${form.name}(${form.typeLabel}${form.managed ? ',受管' : ''}${form.visible ? '' : ',字段被隐藏'})`)
87
- }
88
- if (result.forms.length > 10) lines.push(`- … 其余 ${result.forms.length - 10} 张见文件`)
89
-
90
- const layout = result.views.filter((view) => view.inLayout).length
91
- const filtered = result.views.filter((view) => view.inFilter).length
92
- lines.push('', `视图:${result.views.length} 个(列 ${layout} / 筛选条件 ${filtered})`)
93
- for (const view of result.views.slice(0, 10)) {
94
- const usage = [view.inLayout ? '显示列' : null, view.inFilter ? '筛选条件' : null].filter(Boolean).join('+')
95
- lines.push(`- ${view.name}(${usage}${view.default ? ',默认视图' : ''})`)
96
- }
97
- if (result.views.length > 10) lines.push(`- … 其余 ${result.views.length - 10} 个见文件`)
98
-
99
- lines.push('', `流程/业务规则:${result.automations.length} 个`)
100
- for (const item of result.automations.slice(0, 10)) {
101
- lines.push(`- ${item.name}(${item.category},${item.state})`)
102
- }
103
- if (result.automations.length > 10) lines.push(`- … 其余 ${result.automations.length - 10} 个见文件`)
104
-
105
- lines.push(
106
- '',
107
- total
108
- ? `结论:共 ${total} 处引用;删除或改名会同时影响上述位置。`
109
- : '结论:没有找到引用。注意本工具按实体过滤表单,快速查看表单里引用其它实体字段的情况不在范围内。',
110
- )
111
- return lines.join('\n')
112
- }
113
-
114
- /** Render a web resource report for one environment. */
115
- function renderResource(env, name, forms) {
116
- const lines = [`Web 资源 ${name} 的引用(${env.key})`]
117
- if (!forms.length) {
118
- lines.push('', '结论:没有表单引用这个库。若它是 ribbon 命令或独立加载的脚本,这不代表它没在用。')
119
- return lines.join('\n')
120
- }
121
- lines.push('', `表单:${forms.length} 张`)
122
- let handlers = 0
123
- for (const form of forms.slice(0, 12)) {
124
- handlers += form.handlers.length
125
- const names = form.handlers.map((h) => `${h.event || '?'}→${h.functionName || '?'}`).join(',')
126
- lines.push(`- ${form.name}(${form.typeLabel},${form.entity})${names ? `:${names}` : ''}`)
127
- }
128
- if (forms.length > 12) lines.push(`- … 其余 ${forms.length - 12} 张见文件`)
129
- lines.push('', `结论:${forms.length} 张表单加载该库,共 ${handlers} 个事件处理器;改函数名或删文件会让这些处理器失效。`)
130
- return lines.join('\n')
131
- }
132
-
133
- /** Render a plugin assembly report for one environment. */
134
- function renderAssembly(env, name, result) {
135
- const { assembly, types } = result
136
- const lines = [
137
- `插件程序集 ${assembly.name} 的注册情况(${env.key})`,
138
- `版本:${assembly.version ?? '未知'},隔离模式:${assembly.isolation ?? '?'}`,
139
- `类型:${types.length} 个`,
140
- ]
141
- let stepCount = 0
142
- let disabled = 0
143
- for (const type of types.slice(0, 12)) {
144
- stepCount += type.steps.length
145
- disabled += type.steps.filter((step) => !step.enabled).length
146
- lines.push('', `- ${type.typename}`)
147
- if (!type.steps.length) lines.push(' (没有注册步骤)')
148
- for (const step of type.steps.slice(0, 8)) {
149
- const filter = step.filteringAttributes ? `,过滤 ${step.filteringAttributes}` : ''
150
- lines.push(` · ${step.name}(stage ${step.stage},mode ${step.mode},rank ${step.rank ?? 1}${filter}${step.enabled ? '' : ',已停用'})`)
151
- }
152
- if (type.steps.length > 8) lines.push(` · … 其余 ${type.steps.length - 8} 步见文件`)
153
- }
154
- if (types.length > 12) lines.push(`… 其余 ${types.length - 12} 个类型见文件`)
155
- lines.push('', `结论:共 ${stepCount} 个步骤${disabled ? `,其中 ${disabled} 个已停用` : ''}。`)
156
- return lines.join('\n')
157
- }
158
-
159
- export function register(ctx, state) {
160
- return ctx.tools.register({
161
- ...definition,
162
- async execute(args, exec) {
163
- const config = await loadConfig(state.rawConfig)
164
- const kind = args.kind
165
- if (!KINDS.includes(kind)) {
166
- throw new D365Error(`不认识的查询类型:${kind}`, { code: 'unknown-kind', hint: `可选:${KINDS.join(' / ')}` })
167
- }
168
- if (kind === 'field' && (!args.entity || !args.attribute)) {
169
- throw new D365Error('kind=field 需要 entity 和 attribute', { code: 'missing-field' })
170
- }
171
- if (kind !== 'field' && !args.name) {
172
- throw new D365Error(`kind=${kind} 需要 name`, { code: 'missing-name' })
173
- }
174
-
175
- const envs = pickEnvironments(config, args)
176
- const sections = []
177
- const artifact = { kind, entity: args.entity, attribute: args.attribute, name: args.name, environments: {} }
178
-
179
- for (const env of envs) {
180
- const client = makeClient(env, config)
181
- let text
182
- if (kind === 'field') {
183
- const result = {
184
- forms: await findFieldForms(client, args.entity, args.attribute, exec?.signal),
185
- views: await findFieldViews(client, args.entity, args.attribute, exec?.signal),
186
- automations: await findFieldAutomations(client, args.entity, args.attribute, exec?.signal),
187
- }
188
- artifact.environments[env.key] = result
189
- text = renderField(env, args.entity, args.attribute, result)
190
- } else if (kind === 'web_resource') {
191
- const forms = await findResourceForms(client, args.name, exec?.signal)
192
- artifact.environments[env.key] = { forms }
193
- text = renderResource(env, args.name, forms)
194
- } else {
195
- const result = await findPluginUsage(client, args.name, exec?.signal)
196
- artifact.environments[env.key] = result
197
- text = renderAssembly(env, args.name, result)
198
- }
199
- sections.push(text)
200
- }
201
-
202
- const file = await writeArtifact(config, `refs-${kind}`, artifact)
203
- return { text: `${sections.join('\n\n')}\n\n完整结果:${file}` }
204
- },
205
- })
206
- }
1
+ /**
2
+ * `d365_refs` — the reverse question: who uses this?
3
+ *
4
+ * `d365_metadata` answers "does it exist"; `d365_layers` answers "who overrides
5
+ * it"; this answers "what breaks if I remove or rename it". Every branch reads
6
+ * stored definitions (formxml, layoutxml/fetchxml, workflow clientdata) and
7
+ * reports exact matches, so the answer is evidence rather than a guess.
8
+ */
9
+ import { loadConfig } from './config.js'
10
+ import { makeClient, pickEnvironments, writeArtifact } from './runtime.js'
11
+ import { D365Error } from './errors.js'
12
+ import {
13
+ findFieldAutomations,
14
+ findFieldForms,
15
+ findFieldViews,
16
+ findPluginUsage,
17
+ findResourceForms,
18
+ } from './refs.js'
19
+
20
+ const KINDS = ['field', 'web_resource', 'plugin_assembly']
21
+
22
+ export const definition = {
23
+ name: 'd365_refs',
24
+ description:
25
+ '反查引用:某个字段被哪些表单/视图/流程使用,某个 Web 资源被哪些表单加载,某个插件程序集有哪些注册类型与步骤。'
26
+ + '用于判断"改这个/删这个会不会坏",与 d365_layers(谁覆盖)互补。可一次查多个环境。',
27
+ parameters: {
28
+ type: 'object',
29
+ additionalProperties: false,
30
+ required: ['kind'],
31
+ properties: {
32
+ kind: {
33
+ type: 'string',
34
+ enum: KINDS,
35
+ description: 'field:字段引用;web_resource:表单脚本引用;plugin_assembly:插件注册情况。',
36
+ },
37
+ entity: {
38
+ type: 'string',
39
+ description: 'kind=field 时必填:字段所属实体逻辑名,例如 account。',
40
+ },
41
+ attribute: {
42
+ type: 'string',
43
+ description: 'kind=field 时必填:字段逻辑名,例如 contoso_customertier。',
44
+ },
45
+ name: {
46
+ type: 'string',
47
+ description: 'kind=web_resource 时填 Web 资源名称(如 contoso_/scripts/account_form.js);kind=plugin_assembly 时填程序集名(如 Contoso.Customizations)。',
48
+ },
49
+ environment: {
50
+ type: 'string',
51
+ description: '环境 key,例如 dev。省略则用默认环境。',
52
+ },
53
+ environments: {
54
+ type: 'array',
55
+ items: { type: 'string' },
56
+ description: '一次查多个环境,例如 ["dev","sit"]。',
57
+ },
58
+ allEnvironments: {
59
+ type: 'boolean',
60
+ description: '对全部已配置环境执行同一查询。',
61
+ },
62
+ },
63
+ },
64
+ output: {
65
+ schema: {
66
+ type: 'object',
67
+ additionalProperties: false,
68
+ required: ['text'],
69
+ properties: {
70
+ text: { type: 'string' },
71
+ },
72
+ },
73
+ render(_args, value) {
74
+ return [{ type: 'text', text: value?.text ?? '' }]
75
+ },
76
+ },
77
+ }
78
+
79
+ /** Render a field reference report for one environment. */
80
+ function renderField(env, entity, attribute, result) {
81
+ const lines = [`字段 ${entity}.${attribute} 的引用(${env.key})`]
82
+ const total = result.forms.length + result.views.length + result.automations.length
83
+
84
+ lines.push('', `表单:${result.forms.length} 张`)
85
+ for (const form of result.forms.slice(0, 10)) {
86
+ lines.push(`- ${form.name}(${form.typeLabel}${form.managed ? ',受管' : ''}${form.visible ? '' : ',字段被隐藏'})`)
87
+ }
88
+ if (result.forms.length > 10) lines.push(`- … 其余 ${result.forms.length - 10} 张见文件`)
89
+
90
+ const layout = result.views.filter((view) => view.inLayout).length
91
+ const filtered = result.views.filter((view) => view.inFilter).length
92
+ lines.push('', `视图:${result.views.length} 个(列 ${layout} / 筛选条件 ${filtered})`)
93
+ for (const view of result.views.slice(0, 10)) {
94
+ const usage = [view.inLayout ? '显示列' : null, view.inFilter ? '筛选条件' : null].filter(Boolean).join('+')
95
+ lines.push(`- ${view.name}(${usage}${view.default ? ',默认视图' : ''})`)
96
+ }
97
+ if (result.views.length > 10) lines.push(`- … 其余 ${result.views.length - 10} 个见文件`)
98
+
99
+ lines.push('', `流程/业务规则:${result.automations.length} 个`)
100
+ for (const item of result.automations.slice(0, 10)) {
101
+ lines.push(`- ${item.name}(${item.category},${item.state})`)
102
+ }
103
+ if (result.automations.length > 10) lines.push(`- … 其余 ${result.automations.length - 10} 个见文件`)
104
+
105
+ lines.push(
106
+ '',
107
+ total
108
+ ? `结论:共 ${total} 处引用;删除或改名会同时影响上述位置。`
109
+ : '结论:没有找到引用。注意本工具按实体过滤表单,快速查看表单里引用其它实体字段的情况不在范围内。',
110
+ )
111
+ return lines.join('\n')
112
+ }
113
+
114
+ /** Render a web resource report for one environment. */
115
+ function renderResource(env, name, forms) {
116
+ const lines = [`Web 资源 ${name} 的引用(${env.key})`]
117
+ if (!forms.length) {
118
+ lines.push('', '结论:没有表单引用这个库。若它是 ribbon 命令或独立加载的脚本,这不代表它没在用。')
119
+ return lines.join('\n')
120
+ }
121
+ lines.push('', `表单:${forms.length} 张`)
122
+ let handlers = 0
123
+ for (const form of forms.slice(0, 12)) {
124
+ handlers += form.handlers.length
125
+ const names = form.handlers.map((h) => `${h.event || '?'}→${h.functionName || '?'}`).join(',')
126
+ lines.push(`- ${form.name}(${form.typeLabel},${form.entity})${names ? `:${names}` : ''}`)
127
+ }
128
+ if (forms.length > 12) lines.push(`- … 其余 ${forms.length - 12} 张见文件`)
129
+ lines.push('', `结论:${forms.length} 张表单加载该库,共 ${handlers} 个事件处理器;改函数名或删文件会让这些处理器失效。`)
130
+ return lines.join('\n')
131
+ }
132
+
133
+ /** Render a plugin assembly report for one environment. */
134
+ function renderAssembly(env, name, result) {
135
+ const { assembly, types } = result
136
+ const lines = [
137
+ `插件程序集 ${assembly.name} 的注册情况(${env.key})`,
138
+ `版本:${assembly.version ?? '未知'},隔离模式:${assembly.isolation ?? '?'}`,
139
+ `类型:${types.length} 个`,
140
+ ]
141
+ let stepCount = 0
142
+ let disabled = 0
143
+ for (const type of types.slice(0, 12)) {
144
+ stepCount += type.steps.length
145
+ disabled += type.steps.filter((step) => !step.enabled).length
146
+ lines.push('', `- ${type.typename}`)
147
+ if (!type.steps.length) lines.push(' (没有注册步骤)')
148
+ for (const step of type.steps.slice(0, 8)) {
149
+ const filter = step.filteringAttributes ? `,过滤 ${step.filteringAttributes}` : ''
150
+ lines.push(` · ${step.name}(stage ${step.stage},mode ${step.mode},rank ${step.rank ?? 1}${filter}${step.enabled ? '' : ',已停用'})`)
151
+ }
152
+ if (type.steps.length > 8) lines.push(` · … 其余 ${type.steps.length - 8} 步见文件`)
153
+ }
154
+ if (types.length > 12) lines.push(`… 其余 ${types.length - 12} 个类型见文件`)
155
+ lines.push('', `结论:共 ${stepCount} 个步骤${disabled ? `,其中 ${disabled} 个已停用` : ''}。`)
156
+ return lines.join('\n')
157
+ }
158
+
159
+ export function register(ctx, state) {
160
+ return ctx.tools.register({
161
+ ...definition,
162
+ async execute(args, exec) {
163
+ const config = await loadConfig(state.rawConfig)
164
+ const kind = args.kind
165
+ if (!KINDS.includes(kind)) {
166
+ throw new D365Error(`不认识的查询类型:${kind}`, { code: 'unknown-kind', hint: `可选:${KINDS.join(' / ')}` })
167
+ }
168
+ if (kind === 'field' && (!args.entity || !args.attribute)) {
169
+ throw new D365Error('kind=field 需要 entity 和 attribute', { code: 'missing-field' })
170
+ }
171
+ if (kind !== 'field' && !args.name) {
172
+ throw new D365Error(`kind=${kind} 需要 name`, { code: 'missing-name' })
173
+ }
174
+
175
+ const envs = pickEnvironments(config, args)
176
+ const sections = []
177
+ const artifact = { kind, entity: args.entity, attribute: args.attribute, name: args.name, environments: {} }
178
+
179
+ for (const env of envs) {
180
+ const client = makeClient(env, config)
181
+ let text
182
+ if (kind === 'field') {
183
+ const result = {
184
+ forms: await findFieldForms(client, args.entity, args.attribute, exec?.signal),
185
+ views: await findFieldViews(client, args.entity, args.attribute, exec?.signal),
186
+ automations: await findFieldAutomations(client, args.entity, args.attribute, exec?.signal),
187
+ }
188
+ artifact.environments[env.key] = result
189
+ text = renderField(env, args.entity, args.attribute, result)
190
+ } else if (kind === 'web_resource') {
191
+ const forms = await findResourceForms(client, args.name, exec?.signal)
192
+ artifact.environments[env.key] = { forms }
193
+ text = renderResource(env, args.name, forms)
194
+ } else {
195
+ const result = await findPluginUsage(client, args.name, exec?.signal)
196
+ artifact.environments[env.key] = result
197
+ text = renderAssembly(env, args.name, result)
198
+ }
199
+ sections.push(text)
200
+ }
201
+
202
+ const file = await writeArtifact(config, `refs-${kind}`, artifact)
203
+ return { text: `${sections.join('\n\n')}\n\n完整结果:${file}` }
204
+ },
205
+ })
206
+ }