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,203 +1,203 @@
1
- /**
2
- * Write business records — create one row, or patch fields on one row.
3
- *
4
- * Deliberately narrow: no delete, no bulk update, no upsert by filter. Every
5
- * call is confirmed in the conversation first, because business data is not
6
- * something a plan file can stage ahead of time the way metadata can.
7
- */
8
- import { randomUUID } from 'node:crypto'
9
- import { loadConfig } from './config.js'
10
- import { D365Error } from './errors.js'
11
- import { allows, readPolicy } from './write-policy.js'
12
- import { makeClient, pickEnvironments, writeArtifact } from './runtime.js'
13
- import { resolveEntitySet } from './tool-data.js'
14
-
15
- const APPROVE = '批准执行'
16
- const CANCEL = '取消'
17
- const CONFIRM_ID = 'd365-data-write-confirm'
18
-
19
- const ACTIONS = ['create', 'update']
20
-
21
- const definition = {
22
- name: 'd365_data_write',
23
- description:
24
- '新建或修改表中的一条业务记录(不删除)。需要环境授权「新增/修改业务数据」(data.write);每次写入前会在对话里弹出确认卡片。',
25
- parameters: {
26
- type: 'object',
27
- additionalProperties: false,
28
- required: ['action', 'entity', 'values'],
29
- properties: {
30
- action: {
31
- type: 'string',
32
- enum: ACTIONS,
33
- description: 'create:新建一条;update:按 id 修改已有记录。',
34
- },
35
- entity: {
36
- type: 'string',
37
- description: '实体逻辑名(如 contoso_systemconfiguration)或集合名(如 contoso_systemconfigurations)。',
38
- },
39
- id: {
40
- type: 'string',
41
- description: 'action=update 时必填:记录的主键 GUID。',
42
- },
43
- values: {
44
- type: 'object',
45
- additionalProperties: true,
46
- description: '要写入的字段与值,键用字段逻辑名;值为 null 表示清空。',
47
- },
48
- environment: {
49
- type: 'string',
50
- description: '环境 key,省略则用默认环境。',
51
- },
52
- },
53
- },
54
- output: {
55
- schema: {
56
- type: 'object',
57
- additionalProperties: false,
58
- required: ['text'],
59
- properties: { text: { type: 'string', description: '给人看的摘要。' } },
60
- },
61
- render(_args, value) {
62
- return [{ type: 'text', text: value.text }]
63
- },
64
- },
65
- }
66
-
67
- function clip(value, max = 120) {
68
- const text = typeof value === 'string' ? value : JSON.stringify(value)
69
- if (text === undefined) return 'null'
70
- return text.length > max ? `${text.slice(0, max)}…(${text.length} 字符)` : text
71
- }
72
-
73
- function describeValues(values) {
74
- const entries = Object.entries(values ?? {})
75
- if (!entries.length) return '(没有字段)'
76
- const lines = entries.slice(0, 20).map(([key, value]) => ` ${key} = ${clip(value)}`)
77
- if (entries.length > 20) lines.push(` … 其余 ${entries.length - 20} 个字段`)
78
- return lines.join('\n')
79
- }
80
-
81
- function describeConfirmation(envKey, set, action, id, values) {
82
- const lines = [`环境:${envKey}`, `目标:${set}`, `操作:${action === 'create' ? '新建一条记录' : `修改记录 ${id}`}`]
83
- lines.push('字段:')
84
- lines.push(describeValues(values))
85
- lines.push('业务数据写入不需要发布,也不会生成待确认计划;确认后立即生效。')
86
- return lines.join('\n')
87
- }
88
-
89
- function checkValues(values) {
90
- const entries = Object.entries(values ?? {})
91
- if (!entries.length) throw new D365Error('values 不能为空', { hint: '至少给一个字段,例如 {"name":"Test"}' })
92
- for (const [key] of entries) {
93
- if (key.startsWith('@')) {
94
- throw new D365Error(`字段 ${key} 看起来是 OData 注解,不能写入`, { code: 'invalid-field' })
95
- }
96
- }
97
- return entries.length
98
- }
99
-
100
- export function register(ctx, state) {
101
- return ctx.tools.register({
102
- ...definition,
103
- async execute(args, exec) {
104
- const action = String(args.action ?? '')
105
- if (!ACTIONS.includes(action)) {
106
- throw new D365Error(`未知 action:${action}`, { hint: `可用:${ACTIONS.join(' / ')}` })
107
- }
108
- const config = await loadConfig(state.rawConfig)
109
- const targets = pickEnvironments(config, {
110
- environment: args.environment,
111
- environments: args.environments,
112
- allEnvironments: args.allEnvironments,
113
- })
114
- if (targets.length !== 1) {
115
- throw new D365Error('data.write 一次只能写一个环境', {
116
- hint: `收到 ${targets.length} 个环境:${targets.map((item) => item.key).join(', ')};请用 environment 指定一个。`,
117
- })
118
- }
119
- const env = targets[0]
120
- const envKey = env.key
121
-
122
- const policy = await readPolicy(config.configFile)
123
- if (!allows(policy, envKey, 'data.write')) {
124
- throw new D365Error(`环境 ${envKey} 未授权新增或修改业务数据`, {
125
- code: 'data-write-forbidden',
126
- hint: `在设置页「权限」里给 ${envKey} 勾上「新增/修改业务数据」后再试。`,
127
- })
128
- }
129
-
130
- const values = args.values && typeof args.values === 'object' ? args.values : {}
131
- const fieldCount = checkValues(values)
132
- const client = makeClient(env, config)
133
- const resolved = await resolveEntitySet(client, args.entity, exec?.signal)
134
- const id = String(args.id ?? '').trim()
135
- if (action === 'update' && !id) {
136
- throw new D365Error('update 需要 id', { hint: '传记录的主键 GUID;不确定的话先用 d365_data sample 看一眼。' })
137
- }
138
-
139
- const userQuestions = ctx.get('userQuestions')
140
- if (!userQuestions?.ask) {
141
- throw new D365Error('当前会话没有可用的确认界面', {
142
- hint: '业务数据写入必须在对话里确认;这个工具没有「存起来稍后执行」的路径。',
143
- })
144
- }
145
- if (!exec?.agent) {
146
- throw new D365Error('需要可交互的会话', { hint: '请在主对话里调用。' })
147
- }
148
-
149
- const answer = await userQuestions.ask({
150
- questions: [{
151
- id: CONFIRM_ID,
152
- header: `确认写入数据 ${envKey}`,
153
- question: action === 'create'
154
- ? `要在 ${envKey} 的 ${resolved.set} 新建一条记录吗?`
155
- : `要修改 ${envKey} 的 ${resolved.set}(${id})吗?`,
156
- detail: describeConfirmation(envKey, resolved.set, action, id, values),
157
- options: [
158
- { label: APPROVE, description: '立即写入该环境' },
159
- { label: CANCEL, description: '不执行,什么都不改' },
160
- ],
161
- }],
162
- agent: exec.agent,
163
- ...(exec.signal ? { signal: exec.signal } : {}),
164
- wait: { callId: exec.callId },
165
- })
166
- const chosen = answer?.answers?.find((item) => item.id === CONFIRM_ID)?.selected?.[0]
167
- if (chosen !== APPROVE) return { text: `未执行。${envKey} 的 ${resolved.set} 没有发生任何变化。` }
168
-
169
- let createdId
170
- if (action === 'create') {
171
- const record = await client.sendJson(resolved.set, {
172
- method: 'POST',
173
- body: values,
174
- headers: { Prefer: 'return=representation' },
175
- }, exec.signal)
176
- const entry = Object.entries(record ?? {})
177
- .find(([key, value]) => typeof value === 'string' && (key === resolved.idField || /^\w*id$/i.test(key)))
178
- createdId = entry?.[1]
179
- } else {
180
- await client.sendJson(`${resolved.set}(${id})`, {
181
- method: 'PATCH',
182
- body: values,
183
- headers: { 'If-Match': '*' },
184
- }, exec.signal)
185
- }
186
-
187
- const artifact = {
188
- kind: 'data-write',
189
- at: new Date().toISOString(),
190
- envKey,
191
- entitySet: resolved.set,
192
- action,
193
- id: createdId ?? (action === 'update' ? id : undefined),
194
- fields: Object.keys(values),
195
- }
196
- const path = await writeArtifact(config, `data-write-${randomUUID().slice(0, 8)}`, artifact)
197
- const target = action === 'create'
198
- ? `已在 ${envKey} 的 ${resolved.set} 新建 1 条记录${createdId ? `(id ${createdId})` : ''},${fieldCount} 个字段。`
199
- : `已更新 ${envKey} 的 ${resolved.set}(${id}),${fieldCount} 个字段。`
200
- return { text: `${target}\n\n审计记录只记字段名,不落值:${path}` }
201
- },
202
- })
203
- }
1
+ /**
2
+ * Write business records — create one row, or patch fields on one row.
3
+ *
4
+ * Deliberately narrow: no delete, no bulk update, no upsert by filter. Every
5
+ * call is confirmed in the conversation first, because business data is not
6
+ * something a plan file can stage ahead of time the way metadata can.
7
+ */
8
+ import { randomUUID } from 'node:crypto'
9
+ import { loadConfig } from './config.js'
10
+ import { D365Error } from './errors.js'
11
+ import { allows, readPolicy } from './write-policy.js'
12
+ import { makeClient, pickEnvironments, writeArtifact } from './runtime.js'
13
+ import { resolveEntitySet } from './tool-data.js'
14
+
15
+ const APPROVE = '批准执行'
16
+ const CANCEL = '取消'
17
+ const CONFIRM_ID = 'd365-data-write-confirm'
18
+
19
+ const ACTIONS = ['create', 'update']
20
+
21
+ const definition = {
22
+ name: 'd365_data_write',
23
+ description:
24
+ '新建或修改表中的一条业务记录(不删除)。需要环境授权「新增/修改业务数据」(data.write);每次写入前会在对话里弹出确认卡片。',
25
+ parameters: {
26
+ type: 'object',
27
+ additionalProperties: false,
28
+ required: ['action', 'entity', 'values'],
29
+ properties: {
30
+ action: {
31
+ type: 'string',
32
+ enum: ACTIONS,
33
+ description: 'create:新建一条;update:按 id 修改已有记录。',
34
+ },
35
+ entity: {
36
+ type: 'string',
37
+ description: '实体逻辑名(如 contoso_systemconfiguration)或集合名(如 contoso_systemconfigurations)。',
38
+ },
39
+ id: {
40
+ type: 'string',
41
+ description: 'action=update 时必填:记录的主键 GUID。',
42
+ },
43
+ values: {
44
+ type: 'object',
45
+ additionalProperties: true,
46
+ description: '要写入的字段与值,键用字段逻辑名;值为 null 表示清空。',
47
+ },
48
+ environment: {
49
+ type: 'string',
50
+ description: '环境 key,省略则用默认环境。',
51
+ },
52
+ },
53
+ },
54
+ output: {
55
+ schema: {
56
+ type: 'object',
57
+ additionalProperties: false,
58
+ required: ['text'],
59
+ properties: { text: { type: 'string', description: '给人看的摘要。' } },
60
+ },
61
+ render(_args, value) {
62
+ return [{ type: 'text', text: value.text }]
63
+ },
64
+ },
65
+ }
66
+
67
+ function clip(value, max = 120) {
68
+ const text = typeof value === 'string' ? value : JSON.stringify(value)
69
+ if (text === undefined) return 'null'
70
+ return text.length > max ? `${text.slice(0, max)}…(${text.length} 字符)` : text
71
+ }
72
+
73
+ function describeValues(values) {
74
+ const entries = Object.entries(values ?? {})
75
+ if (!entries.length) return '(没有字段)'
76
+ const lines = entries.slice(0, 20).map(([key, value]) => ` ${key} = ${clip(value)}`)
77
+ if (entries.length > 20) lines.push(` … 其余 ${entries.length - 20} 个字段`)
78
+ return lines.join('\n')
79
+ }
80
+
81
+ function describeConfirmation(envKey, set, action, id, values) {
82
+ const lines = [`环境:${envKey}`, `目标:${set}`, `操作:${action === 'create' ? '新建一条记录' : `修改记录 ${id}`}`]
83
+ lines.push('字段:')
84
+ lines.push(describeValues(values))
85
+ lines.push('业务数据写入不需要发布,也不会生成待确认计划;确认后立即生效。')
86
+ return lines.join('\n')
87
+ }
88
+
89
+ function checkValues(values) {
90
+ const entries = Object.entries(values ?? {})
91
+ if (!entries.length) throw new D365Error('values 不能为空', { hint: '至少给一个字段,例如 {"name":"Test"}' })
92
+ for (const [key] of entries) {
93
+ if (key.startsWith('@')) {
94
+ throw new D365Error(`字段 ${key} 看起来是 OData 注解,不能写入`, { code: 'invalid-field' })
95
+ }
96
+ }
97
+ return entries.length
98
+ }
99
+
100
+ export function register(ctx, state) {
101
+ return ctx.tools.register({
102
+ ...definition,
103
+ async execute(args, exec) {
104
+ const action = String(args.action ?? '')
105
+ if (!ACTIONS.includes(action)) {
106
+ throw new D365Error(`未知 action:${action}`, { hint: `可用:${ACTIONS.join(' / ')}` })
107
+ }
108
+ const config = await loadConfig(state.rawConfig)
109
+ const targets = pickEnvironments(config, {
110
+ environment: args.environment,
111
+ environments: args.environments,
112
+ allEnvironments: args.allEnvironments,
113
+ })
114
+ if (targets.length !== 1) {
115
+ throw new D365Error('data.write 一次只能写一个环境', {
116
+ hint: `收到 ${targets.length} 个环境:${targets.map((item) => item.key).join(', ')};请用 environment 指定一个。`,
117
+ })
118
+ }
119
+ const env = targets[0]
120
+ const envKey = env.key
121
+
122
+ const policy = await readPolicy(config.configFile)
123
+ if (!allows(policy, envKey, 'data.write')) {
124
+ throw new D365Error(`环境 ${envKey} 未授权新增或修改业务数据`, {
125
+ code: 'data-write-forbidden',
126
+ hint: `在设置页「权限」里给 ${envKey} 勾上「新增/修改业务数据」后再试。`,
127
+ })
128
+ }
129
+
130
+ const values = args.values && typeof args.values === 'object' ? args.values : {}
131
+ const fieldCount = checkValues(values)
132
+ const client = makeClient(env, config)
133
+ const resolved = await resolveEntitySet(client, args.entity, exec?.signal)
134
+ const id = String(args.id ?? '').trim()
135
+ if (action === 'update' && !id) {
136
+ throw new D365Error('update 需要 id', { hint: '传记录的主键 GUID;不确定的话先用 d365_data sample 看一眼。' })
137
+ }
138
+
139
+ const userQuestions = ctx.get('userQuestions')
140
+ if (!userQuestions?.ask) {
141
+ throw new D365Error('当前会话没有可用的确认界面', {
142
+ hint: '业务数据写入必须在对话里确认;这个工具没有「存起来稍后执行」的路径。',
143
+ })
144
+ }
145
+ if (!exec?.agent) {
146
+ throw new D365Error('需要可交互的会话', { hint: '请在主对话里调用。' })
147
+ }
148
+
149
+ const answer = await userQuestions.ask({
150
+ questions: [{
151
+ id: CONFIRM_ID,
152
+ header: `确认写入数据 ${envKey}`,
153
+ question: action === 'create'
154
+ ? `要在 ${envKey} 的 ${resolved.set} 新建一条记录吗?`
155
+ : `要修改 ${envKey} 的 ${resolved.set}(${id})吗?`,
156
+ detail: describeConfirmation(envKey, resolved.set, action, id, values),
157
+ options: [
158
+ { label: APPROVE, description: '立即写入该环境' },
159
+ { label: CANCEL, description: '不执行,什么都不改' },
160
+ ],
161
+ }],
162
+ agent: exec.agent,
163
+ ...(exec.signal ? { signal: exec.signal } : {}),
164
+ wait: { callId: exec.callId },
165
+ })
166
+ const chosen = answer?.answers?.find((item) => item.id === CONFIRM_ID)?.selected?.[0]
167
+ if (chosen !== APPROVE) return { text: `未执行。${envKey} 的 ${resolved.set} 没有发生任何变化。` }
168
+
169
+ let createdId
170
+ if (action === 'create') {
171
+ const record = await client.sendJson(resolved.set, {
172
+ method: 'POST',
173
+ body: values,
174
+ headers: { Prefer: 'return=representation' },
175
+ }, exec.signal)
176
+ const entry = Object.entries(record ?? {})
177
+ .find(([key, value]) => typeof value === 'string' && (key === resolved.idField || /^\w*id$/i.test(key)))
178
+ createdId = entry?.[1]
179
+ } else {
180
+ await client.sendJson(`${resolved.set}(${id})`, {
181
+ method: 'PATCH',
182
+ body: values,
183
+ headers: { 'If-Match': '*' },
184
+ }, exec.signal)
185
+ }
186
+
187
+ const artifact = {
188
+ kind: 'data-write',
189
+ at: new Date().toISOString(),
190
+ envKey,
191
+ entitySet: resolved.set,
192
+ action,
193
+ id: createdId ?? (action === 'update' ? id : undefined),
194
+ fields: Object.keys(values),
195
+ }
196
+ const path = await writeArtifact(config, `data-write-${randomUUID().slice(0, 8)}`, artifact)
197
+ const target = action === 'create'
198
+ ? `已在 ${envKey} 的 ${resolved.set} 新建 1 条记录${createdId ? `(id ${createdId})` : ''},${fieldCount} 个字段。`
199
+ : `已更新 ${envKey} 的 ${resolved.set}(${id}),${fieldCount} 个字段。`
200
+ return { text: `${target}\n\n审计记录只记字段名,不落值:${path}` }
201
+ },
202
+ })
203
+ }