dsh-d365-kit 0.34.1 → 0.34.4

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 +319 -308
  2. package/CHANGELOG.zh-CN.md +318 -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
package/lib/health.js CHANGED
@@ -1,205 +1,205 @@
1
- /**
2
- * Environment health: what is running right now, what failed recently, and how
3
- * busy the org looks. Meant to answer "is it safe to publish?" before a release.
4
- */
5
-
6
- import { D365Error } from './errors.js'
7
- import { makeClient, readOrganization } from './runtime.js'
8
-
9
- /**
10
- * `asyncoperation.statecode`: 0 Ready (queued), 1 Suspended, 2 Locked,
11
- * 3 Completed. Completed is *not* failure: `statuscode` splits it into
12
- * 30 Succeeded / 31 Failed / 32 Canceled, so "failed" needs both codes.
13
- */
14
- const READY_STATE = 0
15
- const SUSPENDED_STATE = 1
16
- const COMPLETED_STATE = 3
17
- const FAILED_STATUS = 31
18
-
19
- const FAILED_FILTER = `statecode eq ${COMPLETED_STATE} and statuscode eq ${FAILED_STATUS}`
20
-
21
- async function count(client, path, filter, signal) {
22
- // `$count=true` is capped at 5000 by Dataverse; aggregation is exact.
23
- try {
24
- const url = client.buildUrl(path, { $filter: filter, $apply: 'aggregate($count as count)' })
25
- const page = await client.requestJson(url, { signal })
26
- const value = page?.value?.[0]?.count
27
- if (Number.isFinite(value)) return { count: value }
28
- } catch {
29
- // fall through to the simpler read
30
- }
31
- const url = client.buildUrl(path, { $filter: filter, $select: 'asyncoperationid', $top: 1, $count: 'true' })
32
- const page = await client.requestJson(url, { signal })
33
- return { count: page?.['@odata.count'] ?? page?.value?.length ?? 0, rows: page?.value ?? [] }
34
- }
35
-
36
- async function listFailed(client, sinceIso, signal) {
37
- const rows = []
38
- let next = client.buildUrl('asyncoperations', {
39
- $select: 'asyncoperationid,name,operationtype,errorcode,message,modifiedon',
40
- $filter: `${FAILED_FILTER} and modifiedon ge ${sinceIso}`,
41
- $orderby: 'modifiedon desc',
42
- $top: 50,
43
- })
44
- let pages = 0
45
- while (next && pages < 3) {
46
- const page = await client.requestJson(next, { signal })
47
- rows.push(...(page?.value ?? []))
48
- next = page?.['@odata.nextLink'] ?? null
49
- pages += 1
50
- }
51
- return rows
52
- }
53
-
54
- async function listRunningSolutions(client, signal) {
55
- const url = client.buildUrl('msdyn_solutionhistories', {
56
- $select: 'msdyn_name,msdyn_operation,msdyn_starttime,msdyn_endtime,msdyn_ismanaged',
57
- $filter: 'msdyn_endtime eq null',
58
- $orderby: 'msdyn_starttime desc',
59
- $top: 10,
60
- })
61
- const page = await client.requestJson(url, { signal })
62
- return page?.value ?? []
63
- }
64
-
65
- async function recentSolutions(client, signal) {
66
- const url = client.buildUrl('solutions', {
67
- $select: 'uniquename,friendlyname,version,ismanaged,installedon',
68
- $orderby: 'installedon desc',
69
- $top: 5,
70
- })
71
- const page = await client.requestJson(url, { signal })
72
- return page?.value ?? []
73
- }
74
-
75
- /** Collect one environment's health snapshot. */
76
- export async function readHealth(config, envKey, options = {}) {
77
- const env = config.environments[envKey]
78
- if (!env) throw new D365Error(`未知环境:${envKey}`, { hint: `可用:${Object.keys(config.environments).join(', ')}` })
79
- const hours = Number.isFinite(options.hours) ? options.hours : 24
80
- const sinceIso = new Date(Date.now() - hours * 3600 * 1000).toISOString()
81
- const client = makeClient(env, config)
82
- const signal = options.signal
83
-
84
- const version = await client.readVersion().catch(() => null)
85
- const organization = await readOrganization(client, signal).catch(() => null)
86
- // A swallowed permission error must not read as "0 failures": this tool is
87
- // asked precisely when something is wrong, and a false all-clear is worse
88
- // than a visible gap. Track whether the async-job reads were refused.
89
- const refused = (error) => error?.code === 'data-read-forbidden' || error?.status === 401 || error?.status === 403
90
- const jobErrors = []
91
- const jobCount = (filter) => count(client, 'asyncoperations', filter, signal)
92
- .then((value) => ({ ...value, error: null }))
93
- .catch((error) => {
94
- jobErrors.push(error)
95
- return { count: null, error }
96
- })
97
- const queued = await jobCount(`statecode eq ${READY_STATE}`)
98
- const suspended = await jobCount(`statecode eq ${SUSPENDED_STATE}`)
99
- const failed = await jobCount(`${FAILED_FILTER} and modifiedon ge ${sinceIso}`)
100
- const failures = await listFailed(client, sinceIso, signal).catch((error) => {
101
- jobErrors.push(error)
102
- return []
103
- })
104
- const denied = jobErrors.some(refused)
105
- const warnings = denied
106
- ? ['读取异步作业需要「读取业务数据」(data.read);本环境的排队 / 挂起 / 失败统计不可用,显示为 —。']
107
- : []
108
- const failureGroups = new Map()
109
- for (const row of failures) {
110
- const name = String(row.name ?? '')
111
- const key = `${name}|${row.errorcode ?? ''}`
112
- const group = failureGroups.get(key) ?? {
113
- name,
114
- message: String(row.message ?? ''),
115
- operationtype: Number(row.operationtype ?? 0),
116
- errorcode: Number(row.errorcode ?? 0),
117
- count: 0,
118
- modifiedon: String(row.modifiedon ?? ''),
119
- }
120
- group.count += 1
121
- failureGroups.set(key, group)
122
- }
123
- const running = await listRunningSolutions(client, signal).catch(() => [])
124
- const recent = await recentSolutions(client, signal).catch(() => [])
125
- // Without this the most common reason a trace table is empty ("nobody turned
126
- // plug-in tracing on") is invisible: the tool reports "0 logs" and stops.
127
- const traceSetting = Number(organization?.plugintracelogsetting)
128
-
129
- return {
130
- env: envKey,
131
- url: env.url ?? '',
132
- version: version ?? '',
133
- organization: organization?.name ?? '',
134
- traceLevel: Number.isFinite(traceSetting) ? traceSetting : -1,
135
- queued: queued.count ?? 0,
136
- suspended: suspended.count ?? 0,
137
- failed: failed.count ?? 0,
138
- denied,
139
- warnings,
140
- hours,
141
- failures: [...failureGroups.values()].map((row) => ({
142
- name: row.name,
143
- message: row.message,
144
- operationtype: row.operationtype,
145
- modifiedon: row.modifiedon,
146
- count: row.count,
147
- errorcode: row.errorcode,
148
- })),
149
- runningSolutions: running.map((row) => ({
150
- name: String(row.msdyn_name ?? ''),
151
- operation: Number(row.msdyn_operation ?? 0),
152
- startedAt: String(row.msdyn_starttime ?? ''),
153
- managed: row.msdyn_ismanaged === true,
154
- })),
155
- recentSolutions: recent.map((row) => ({
156
- uniquename: String(row.uniquename ?? ''),
157
- version: String(row.version ?? ''),
158
- managed: row.ismanaged === true,
159
- installedon: String(row.installedon ?? ''),
160
- })),
161
- }
162
- }
163
-
164
- const TRACE_LEVELS = { 0: '关闭', 1: '仅异常', 2: '全部' }
165
-
166
- /** @returns a readable label for the organization's plug-in trace setting. */
167
- export function traceLabel(level) {
168
- if (level === null || level === undefined || level < 0) return '—'
169
- return TRACE_LEVELS[level] ?? `未知(${level})`
170
- }
171
-
172
- export function renderHealth(rows) {
173
- const lines = []
174
- lines.push('环境健康检查')
175
- lines.push('')
176
- lines.push('| 环境 | 版本 | 组织 | 排队中 | 挂起作业 | 近期失败 | 正在导入/发布 | 插件跟踪 |')
177
- lines.push('| --- | --- | --- | --- | --- | --- | --- | --- |')
178
- for (const row of rows) {
179
- const show = (value) => (row.denied ? '—' : (value ?? '—'))
180
- lines.push(`| ${row.env} | ${row.version ?? '—'} | ${row.organization ?? '—'} | ${show(row.queued)} | ${show(row.suspended)} | ${show(row.failed)} | ${row.runningSolutions.length} | ${traceLabel(row.traceLevel)} |`)
181
- }
182
- for (const row of rows) {
183
- for (const warning of row.warnings ?? []) lines.push(`- ${row.env}:${warning}`)
184
- }
185
- lines.push('')
186
- lines.push('排队中 = Ready(statecode 0);挂起 = Suspended(statecode 1);近期失败 = Completed 且 Failed(statuscode 31),Succeeded/Canceled 不计。')
187
- lines.push('插件跟踪 = 组织的 Plug-in Trace Log 设置:关闭时插件异常只写系统作业、不写跟踪日志,查不到日志先看这一列。')
188
- for (const row of rows) {
189
- if (!row.runningSolutions.length && !row.failures.length) continue
190
- lines.push(`### ${row.env}`)
191
- if (row.runningSolutions.length) {
192
- lines.push(`- 正在运行:${row.runningSolutions.map((item) => `${item.name}(${item.operation})`).join('、')}`)
193
- }
194
- for (const failure of row.failures.slice(0, 5)) {
195
- const times = failure.count > 1 ? ` ×${failure.count}` : ''
196
- const code = failure.errorcode ? `(code ${failure.errorcode})` : ''
197
- // `message` is often empty on async operations; a trailing dash reads as a
198
- // rendering bug, so only add the separator when there is text to show.
199
- const detail = String(failure.message ?? '').trim()
200
- lines.push(`- 失败${times}:${failure.name}${code}${detail ? ` — ${detail.slice(0, 160)}` : ''}`)
201
- }
202
- lines.push('')
203
- }
204
- return [{ type: 'text', text: lines.join('\n') }]
205
- }
1
+ /**
2
+ * Environment health: what is running right now, what failed recently, and how
3
+ * busy the org looks. Meant to answer "is it safe to publish?" before a release.
4
+ */
5
+
6
+ import { D365Error } from './errors.js'
7
+ import { makeClient, readOrganization } from './runtime.js'
8
+
9
+ /**
10
+ * `asyncoperation.statecode`: 0 Ready (queued), 1 Suspended, 2 Locked,
11
+ * 3 Completed. Completed is *not* failure: `statuscode` splits it into
12
+ * 30 Succeeded / 31 Failed / 32 Canceled, so "failed" needs both codes.
13
+ */
14
+ const READY_STATE = 0
15
+ const SUSPENDED_STATE = 1
16
+ const COMPLETED_STATE = 3
17
+ const FAILED_STATUS = 31
18
+
19
+ const FAILED_FILTER = `statecode eq ${COMPLETED_STATE} and statuscode eq ${FAILED_STATUS}`
20
+
21
+ async function count(client, path, filter, signal) {
22
+ // `$count=true` is capped at 5000 by Dataverse; aggregation is exact.
23
+ try {
24
+ const url = client.buildUrl(path, { $filter: filter, $apply: 'aggregate($count as count)' })
25
+ const page = await client.requestJson(url, { signal })
26
+ const value = page?.value?.[0]?.count
27
+ if (Number.isFinite(value)) return { count: value }
28
+ } catch {
29
+ // fall through to the simpler read
30
+ }
31
+ const url = client.buildUrl(path, { $filter: filter, $select: 'asyncoperationid', $top: 1, $count: 'true' })
32
+ const page = await client.requestJson(url, { signal })
33
+ return { count: page?.['@odata.count'] ?? page?.value?.length ?? 0, rows: page?.value ?? [] }
34
+ }
35
+
36
+ async function listFailed(client, sinceIso, signal) {
37
+ const rows = []
38
+ let next = client.buildUrl('asyncoperations', {
39
+ $select: 'asyncoperationid,name,operationtype,errorcode,message,modifiedon',
40
+ $filter: `${FAILED_FILTER} and modifiedon ge ${sinceIso}`,
41
+ $orderby: 'modifiedon desc',
42
+ $top: 50,
43
+ })
44
+ let pages = 0
45
+ while (next && pages < 3) {
46
+ const page = await client.requestJson(next, { signal })
47
+ rows.push(...(page?.value ?? []))
48
+ next = page?.['@odata.nextLink'] ?? null
49
+ pages += 1
50
+ }
51
+ return rows
52
+ }
53
+
54
+ async function listRunningSolutions(client, signal) {
55
+ const url = client.buildUrl('msdyn_solutionhistories', {
56
+ $select: 'msdyn_name,msdyn_operation,msdyn_starttime,msdyn_endtime,msdyn_ismanaged',
57
+ $filter: 'msdyn_endtime eq null',
58
+ $orderby: 'msdyn_starttime desc',
59
+ $top: 10,
60
+ })
61
+ const page = await client.requestJson(url, { signal })
62
+ return page?.value ?? []
63
+ }
64
+
65
+ async function recentSolutions(client, signal) {
66
+ const url = client.buildUrl('solutions', {
67
+ $select: 'uniquename,friendlyname,version,ismanaged,installedon',
68
+ $orderby: 'installedon desc',
69
+ $top: 5,
70
+ })
71
+ const page = await client.requestJson(url, { signal })
72
+ return page?.value ?? []
73
+ }
74
+
75
+ /** Collect one environment's health snapshot. */
76
+ export async function readHealth(config, envKey, options = {}) {
77
+ const env = config.environments[envKey]
78
+ if (!env) throw new D365Error(`未知环境:${envKey}`, { hint: `可用:${Object.keys(config.environments).join(', ')}` })
79
+ const hours = Number.isFinite(options.hours) ? options.hours : 24
80
+ const sinceIso = new Date(Date.now() - hours * 3600 * 1000).toISOString()
81
+ const client = makeClient(env, config)
82
+ const signal = options.signal
83
+
84
+ const version = await client.readVersion().catch(() => null)
85
+ const organization = await readOrganization(client, signal).catch(() => null)
86
+ // A swallowed permission error must not read as "0 failures": this tool is
87
+ // asked precisely when something is wrong, and a false all-clear is worse
88
+ // than a visible gap. Track whether the async-job reads were refused.
89
+ const refused = (error) => error?.code === 'data-read-forbidden' || error?.status === 401 || error?.status === 403
90
+ const jobErrors = []
91
+ const jobCount = (filter) => count(client, 'asyncoperations', filter, signal)
92
+ .then((value) => ({ ...value, error: null }))
93
+ .catch((error) => {
94
+ jobErrors.push(error)
95
+ return { count: null, error }
96
+ })
97
+ const queued = await jobCount(`statecode eq ${READY_STATE}`)
98
+ const suspended = await jobCount(`statecode eq ${SUSPENDED_STATE}`)
99
+ const failed = await jobCount(`${FAILED_FILTER} and modifiedon ge ${sinceIso}`)
100
+ const failures = await listFailed(client, sinceIso, signal).catch((error) => {
101
+ jobErrors.push(error)
102
+ return []
103
+ })
104
+ const denied = jobErrors.some(refused)
105
+ const warnings = denied
106
+ ? ['读取异步作业需要「读取业务数据」(data.read);本环境的排队 / 挂起 / 失败统计不可用,显示为 —。']
107
+ : []
108
+ const failureGroups = new Map()
109
+ for (const row of failures) {
110
+ const name = String(row.name ?? '')
111
+ const key = `${name}|${row.errorcode ?? ''}`
112
+ const group = failureGroups.get(key) ?? {
113
+ name,
114
+ message: String(row.message ?? ''),
115
+ operationtype: Number(row.operationtype ?? 0),
116
+ errorcode: Number(row.errorcode ?? 0),
117
+ count: 0,
118
+ modifiedon: String(row.modifiedon ?? ''),
119
+ }
120
+ group.count += 1
121
+ failureGroups.set(key, group)
122
+ }
123
+ const running = await listRunningSolutions(client, signal).catch(() => [])
124
+ const recent = await recentSolutions(client, signal).catch(() => [])
125
+ // Without this the most common reason a trace table is empty ("nobody turned
126
+ // plug-in tracing on") is invisible: the tool reports "0 logs" and stops.
127
+ const traceSetting = Number(organization?.plugintracelogsetting)
128
+
129
+ return {
130
+ env: envKey,
131
+ url: env.url ?? '',
132
+ version: version ?? '',
133
+ organization: organization?.name ?? '',
134
+ traceLevel: Number.isFinite(traceSetting) ? traceSetting : -1,
135
+ queued: queued.count ?? 0,
136
+ suspended: suspended.count ?? 0,
137
+ failed: failed.count ?? 0,
138
+ denied,
139
+ warnings,
140
+ hours,
141
+ failures: [...failureGroups.values()].map((row) => ({
142
+ name: row.name,
143
+ message: row.message,
144
+ operationtype: row.operationtype,
145
+ modifiedon: row.modifiedon,
146
+ count: row.count,
147
+ errorcode: row.errorcode,
148
+ })),
149
+ runningSolutions: running.map((row) => ({
150
+ name: String(row.msdyn_name ?? ''),
151
+ operation: Number(row.msdyn_operation ?? 0),
152
+ startedAt: String(row.msdyn_starttime ?? ''),
153
+ managed: row.msdyn_ismanaged === true,
154
+ })),
155
+ recentSolutions: recent.map((row) => ({
156
+ uniquename: String(row.uniquename ?? ''),
157
+ version: String(row.version ?? ''),
158
+ managed: row.ismanaged === true,
159
+ installedon: String(row.installedon ?? ''),
160
+ })),
161
+ }
162
+ }
163
+
164
+ const TRACE_LEVELS = { 0: '关闭', 1: '仅异常', 2: '全部' }
165
+
166
+ /** @returns a readable label for the organization's plug-in trace setting. */
167
+ export function traceLabel(level) {
168
+ if (level === null || level === undefined || level < 0) return '—'
169
+ return TRACE_LEVELS[level] ?? `未知(${level})`
170
+ }
171
+
172
+ export function renderHealth(rows) {
173
+ const lines = []
174
+ lines.push('环境健康检查')
175
+ lines.push('')
176
+ lines.push('| 环境 | 版本 | 组织 | 排队中 | 挂起作业 | 近期失败 | 正在导入/发布 | 插件跟踪 |')
177
+ lines.push('| --- | --- | --- | --- | --- | --- | --- | --- |')
178
+ for (const row of rows) {
179
+ const show = (value) => (row.denied ? '—' : (value ?? '—'))
180
+ lines.push(`| ${row.env} | ${row.version ?? '—'} | ${row.organization ?? '—'} | ${show(row.queued)} | ${show(row.suspended)} | ${show(row.failed)} | ${row.runningSolutions.length} | ${traceLabel(row.traceLevel)} |`)
181
+ }
182
+ for (const row of rows) {
183
+ for (const warning of row.warnings ?? []) lines.push(`- ${row.env}:${warning}`)
184
+ }
185
+ lines.push('')
186
+ lines.push('排队中 = Ready(statecode 0);挂起 = Suspended(statecode 1);近期失败 = Completed 且 Failed(statuscode 31),Succeeded/Canceled 不计。')
187
+ lines.push('插件跟踪 = 组织的 Plug-in Trace Log 设置:关闭时插件异常只写系统作业、不写跟踪日志,查不到日志先看这一列。')
188
+ for (const row of rows) {
189
+ if (!row.runningSolutions.length && !row.failures.length) continue
190
+ lines.push(`### ${row.env}`)
191
+ if (row.runningSolutions.length) {
192
+ lines.push(`- 正在运行:${row.runningSolutions.map((item) => `${item.name}(${item.operation})`).join('、')}`)
193
+ }
194
+ for (const failure of row.failures.slice(0, 5)) {
195
+ const times = failure.count > 1 ? ` ×${failure.count}` : ''
196
+ const code = failure.errorcode ? `(code ${failure.errorcode})` : ''
197
+ // `message` is often empty on async operations; a trailing dash reads as a
198
+ // rendering bug, so only add the separator when there is text to show.
199
+ const detail = String(failure.message ?? '').trim()
200
+ lines.push(`- 失败${times}:${failure.name}${code}${detail ? ` — ${detail.slice(0, 160)}` : ''}`)
201
+ }
202
+ lines.push('')
203
+ }
204
+ return [{ type: 'text', text: lines.join('\n') }]
205
+ }