fix: текстовый BOM работает в пасте конфигуратора через единый серверный парсер

Паста BOM на странице конфигурирования теперь распознаёт текстовый и
Inspur-форматы: вместо дублирования парсера на JS добавлен stateless
эндпоинт POST /api/vendor-spec/parse-text, который использует те же
детекторы и парсеры, что и импорт файла (KISS — один парсер на оба
входа). JS-копии _parseInspurBOMText/_isInspurBOMText удалены.

Заголовок конфигурации определяется по маркеру ", в составе:" с любым
префиксом ("Сервер X3" и "Вычислительный GPU сервер X3" → модель X3);
строки тримятся, пробел в начале не попадает в P/N; запятые и дефисы
внутри описания сохраняются (RAID0,1,10; 8-GPU-2304GB).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-06-16 09:16:55 +03:00
parent 6f2c261350
commit 24c34eb0e1
6 changed files with 235 additions and 58 deletions
+32 -3
View File
@@ -690,8 +690,35 @@ func parseInspurBOM(data []byte, sourceFileName string) (*importedWorkspace, err
// description (e.g. "8-GPU-2304GB") from being mistaken for the separator.
var textBOMItemLine = regexp.MustCompile(`(?i)^(.*\S)\s*[-–—]\s*(\d+)\s*шт\.?\s*$`)
// textBOMHeaderLine matches the configuration header "Сервер <model>, в составе:".
var textBOMHeaderLine = regexp.MustCompile(`(?i)^\s*сервер\s+(.+?)\s*,\s*в\s+составе`)
// textBOMHeaderLine matches a configuration header ending with ", в составе:"
// regardless of the leading words (e.g. "Сервер <model>" or
// "Вычислительный GPU сервер <model>"). The captured group is everything before
// the comma; the model is its last whitespace-separated token.
var textBOMHeaderLine = regexp.MustCompile(`(?i)^(.*?)\s*,\s*в\s+составе`)
// ParsePastedBOMText detects and parses a single-column text BOM (Inspur or
// Russian text BOM) pasted into the configurator. It shares the same detectors
// and parsers as the vendor file-import path, so paste and upload behave
// identically. It returns the parsed vendor spec rows and the detected format,
// or (nil, "") when the text is not a recognized single-column format and the
// caller should fall back to manual column mapping.
func ParsePastedBOMText(text string) ([]localdb.VendorSpecItem, string) {
data := []byte(text)
var ws *importedWorkspace
var err error
switch {
case IsInspurBOM(data):
ws, err = parseInspurBOM(data, "")
case IsTextBOM(data):
ws, err = parseTextBOM(data, "")
default:
return nil, ""
}
if err != nil || ws == nil || len(ws.Configurations) == 0 {
return nil, ""
}
return ws.Configurations[0].Rows, ws.SourceFormat
}
// IsTextBOM reports whether data looks like a human-readable Russian text BOM,
// i.e. it contains at least one "<description> - <quantity> шт." line.
@@ -721,7 +748,9 @@ func parseTextBOM(data []byte, sourceFileName string) (*importedWorkspace, error
continue
}
if m := textBOMHeaderLine.FindStringSubmatch(line); m != nil {
serverModel = strings.TrimSpace(m[1])
if fields := strings.Fields(m[1]); len(fields) > 0 {
serverModel = fields[len(fields)-1]
}
continue
}
m := textBOMItemLine.FindStringSubmatch(line)