@us_momo/dsh-erp-analysis 1.8.0 → 1.10.0

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.
@@ -14,7 +14,7 @@ export function selectionCapability(tools) {
14
14
 
15
15
  export const researchSaveTool = {
16
16
  name: 'erp_selection_research_save',
17
- description: '用户明确保存后,将本机研究草稿分步写入ERP并返回可读链接。request含固定operationId(UUID)和path(JSON草稿)。草稿为{direction:{id?,expectedVersion?,name?,market?,summary?,relatedId?},report:{id?,expectedVersion?,title?,sections,references?},materials:[{key,path,sourceNote?,site?,collectedOn?}]}。新主题给name;接续先read,给direction.id,已有主报告须给report.id/expectedVersion,仅写本轮章节。Markdown图片用![说明](material:唯一key),材料路径相对草稿文件;保存后自动上传并换为ERP图片地址。每步固定操作号,结果未知再次调用原operationId和原草稿,恢复成功步骤;不会重复建整套档案。普通研究讨论不调用本工具。',
17
+ description: '用户明确保存后,将本机研究草稿分步写入ERP并返回可读链接。request含固定operationId(UUID)和path(JSON草稿)。草稿为{direction:{id?,expectedVersion?,name?,market?,summary?,relatedId?},report:{id?,expectedVersion?,title?,sections?,references?,comparison?},materials:[{key,path,sourceNote?,site?,collectedOn?}]}。新主题给name;接续先read,给direction.id,已有主报告须给report.id/expectedVersion,仅写本轮章节。comparison为{title?,basis?,dimensions?,groups:[{name,summary?,features?,samples?,evidence?,notes?}]};samples用{name,identifier?,url?,imageKey?},imageKey关联materials,保存时换成imageFileId;已有图片可用imageFileId。Markdown图片用![说明](material:唯一key),材料路径相对草稿文件;保存后自动上传并换为ERP图片地址。每步固定操作号,结果未知再次调用原operationId和原草稿,恢复成功步骤;不会重复建整套档案。普通研究讨论不调用本工具。',
18
18
  inputSchema: { type: 'object', additionalProperties: false, required: ['request'], properties: {
19
19
  request: { type: 'object', additionalProperties: false, required: ['operationId', 'path'], properties: {
20
20
  operationId: { type: 'string', format: 'uuid' }, path: { type: 'string' },
@@ -55,10 +55,26 @@ export async function saveResearch(client, root, { request }) {
55
55
  const draft = JSON.parse(await readFile(path, 'utf8'));
56
56
  fields(draft, ['direction', 'report', 'materials'], '研究');
57
57
  fields(draft.direction, ['id', 'expectedVersion', 'name', 'market', 'summary', 'relatedId'], '主题/方向');
58
- fields(draft.report, ['id', 'expectedVersion', 'title', 'sections', 'references'], '报告');
58
+ fields(draft.report, ['id', 'expectedVersion', 'title', 'sections', 'references', 'comparison'], '报告');
59
+ draft.report.sections ??= {};
59
60
  fields(draft.report.sections, ['scope', 'types', 'prices', 'needs', 'trends', 'conclusion', 'sourcing'], '章节');
60
- if (!Object.keys(draft.report.sections).length || Object.values(draft.report.sections).some(value => typeof value !== 'string' || value.length > 60000))
61
+ if (!Object.keys(draft.report.sections).length && draft.report.comparison === undefined || Object.values(draft.report.sections).some(value => typeof value !== 'string' || value.length > 60000))
61
62
  throw new Error('请提供本轮研究章节文字,每章最多 60000 字符。');
63
+ const comparisonSamples = [];
64
+ if (draft.report.comparison != null) {
65
+ fields(draft.report.comparison, ['title', 'basis', 'dimensions', 'groups'], '图文对照');
66
+ if (!Array.isArray(draft.report.comparison.groups) || !draft.report.comparison.groups.length || draft.report.comparison.groups.length > 60)
67
+ throw new Error('图文对照须包含1到60个类型。');
68
+ for (const group of draft.report.comparison.groups) {
69
+ fields(group, ['name', 'summary', 'features', 'samples', 'evidence', 'notes'], '类型');
70
+ if (!Array.isArray(group.samples ?? [])) throw new Error('代表商品须为数组。');
71
+ for (const sample of group.samples ?? []) {
72
+ fields(sample, ['name', 'identifier', 'url', 'imageKey', 'imageFileId'], '代表商品');
73
+ if (sample.imageKey && sample.imageFileId) throw new Error('代表图片请选择本轮imageKey或已保存imageFileId。');
74
+ comparisonSamples.push(sample);
75
+ }
76
+ }
77
+ }
62
78
  if (draft.report.references !== undefined && !Array.isArray(draft.report.references)) throw new Error('报告引用须为数组。');
63
79
  if (draft.direction.id && !uuid(draft.direction.id) || !draft.direction.id && (typeof draft.direction.name !== 'string' || !draft.direction.name.trim()))
64
80
  throw new Error('新研究须有名称;接续须有正确主题/方向编号。');
@@ -77,7 +93,9 @@ export async function saveResearch(client, root, { request }) {
77
93
  for (const match of markdown.matchAll(/\(material:([a-zA-Z0-9_-]+)\)/g))
78
94
  if (!keys.has(match[1])) throw new Error('章节引用了未提供的材料key。');
79
95
  const illustrations = materials.filter(item => /\.(png|jpe?g|webp)$/i.test(item.filename));
80
- const unplaced = illustrations.filter(item => !markdown.includes(`](material:${item.key})`));
96
+ for (const sample of comparisonSamples) if (sample.imageKey && !illustrations.some(item => item.key === sample.imageKey))
97
+ throw new Error('代表商品imageKey须指向本轮提供的图片材料。');
98
+ const unplaced = illustrations.filter(item => !markdown.includes(`](material:${item.key})`) && !comparisonSamples.some(sample => sample.imageKey === item.key));
81
99
  if (unplaced.length && draft.report.sections.types === undefined) throw new Error('请提供types章节,用于展示本轮代表图片。');
82
100
  for (const match of markdown.matchAll(/!\[[^\]]*\]\(([^\s)]+)(?:\s+[^)]*)?\)/g)) {
83
101
  const url = match[1];
@@ -135,13 +153,22 @@ export async function saveResearch(client, root, { request }) {
135
153
  value.replace(/\(material:([a-zA-Z0-9_-]+)\)/g, (_, key) => `(/api/v1/selection/files/${fileIds[key]}?inline=1)`)]));
136
154
  if (unplaced.length) sections.types += '\n\n### 代表图片\n' + unplaced.map(item =>
137
155
  `\n![${(item.sourceNote || item.filename).replace(/[\[\]\r\n]/g, ' ')}](/api/v1/selection/files/${fileIds[item.key]}?inline=1)\n\n${item.sourceNote || item.filename}\n`).join('\n');
156
+ const comparison = draft.report.comparison == null ? draft.report.comparison : {
157
+ ...draft.report.comparison, groups: draft.report.comparison.groups.map(group => ({ ...group,
158
+ ...(group.samples ? { samples: group.samples.map(({ imageKey, ...sample }) => ({ ...sample,
159
+ ...(imageKey ? { imageFileId: fileIds[imageKey] } : {}) })) } : {}),
160
+ })),
161
+ };
138
162
  const allReferences = [...(existingMain?.references ?? []), ...(draft.report.references ?? []), ...materials.map(item => ({ title: item.filename, fileId: fileIds[item.key] }))];
139
163
  const references = allReferences.filter((item, index) => allReferences.findIndex(other => JSON.stringify(other) === JSON.stringify(item)) === index);
140
164
  const report = journal.steps.report?.item ?? await step('report', 'report', { ...(draft.report.id ? {} : { directionId: direction.id, kind: 'main' }),
141
- ...(draft.report.title !== undefined ? { title: draft.report.title } : {}), sections, references }, draft.report.id ? draft.report : undefined);
165
+ ...(draft.report.title !== undefined ? { title: draft.report.title } : {}), sections, references,
166
+ ...(comparison !== undefined ? { comparison } : {}) }, draft.report.id ? draft.report : undefined);
142
167
  const saved = await call('erp_selection_read', { request: { directionId: direction.id } });
143
168
  if (!saved.reports.some(item => item.id === report.id) || materials.some(item => !saved.files.some(file => file.id === fileIds[item.key])))
144
169
  throw new Error('保存已受理,但回读未核对完成,请用原请求恢复。');
170
+ if (comparison !== undefined && !!saved.reports.find(item => item.id === report.id)?.comparison !== !!comparison)
171
+ throw new Error('图文对照回读不一致,请用原请求恢复并核对。');
145
172
  return { status: 'saved', operationId: request.operationId, directionId: direction.id, reportId: report.id, reportVersion: report.version,
146
173
  fileIds, url: new URL('/workspace/selection?directionId=' + direction.id, client.base).href };
147
174
  } catch (error) {