Files
LWLT-AIBOT/chrome-extension/ltjt-order-assistant/team-batch.js
2026-08-25 10:33:29 +08:00

289 lines
11 KiB
JavaScript

(function installLTJTTeamBatchHelpers(root, factory) {
const api = factory();
if (typeof module !== 'undefined' && module.exports) module.exports = api;
if (root) root.LTJTTeamBatchHelpers = api;
}(typeof self !== 'undefined' ? self : globalThis, () => {
function normalizeText(value) {
return String(value ?? '')
.normalize('NFKC')
.toLowerCase()
.replace(/[\s\p{P}\p{S}]+/gu, '')
.trim();
}
function chineseOrLatinSegments(value) {
const normalized = String(value ?? '').normalize('NFKC').toLowerCase();
return normalized.match(/[\p{Script=Han}]+|[a-z0-9]+/giu) || [];
}
function tokenizeKeyword(value) {
const tokens = [];
for (const segment of chineseOrLatinSegments(value)) {
if (/^[\p{Script=Han}]+$/u.test(segment)) {
if (segment.length <= 2) {
tokens.push(segment);
continue;
}
for (let index = 0; index < segment.length; index += 2) {
tokens.push(segment.slice(index, index + 2));
}
} else if (segment) {
tokens.push(segment);
}
}
return [...new Set(tokens.map(normalizeText).filter(Boolean))];
}
function rowColumns(row) {
return String(row ?? '').split('◆').map((value) => value.trim());
}
function parseSelectBoxRows(serialized) {
return String(serialized ?? '')
.split(String(serialized ?? '').includes('◇') ? '◇' : /\r?\n/)
.map((raw, rowIndex) => ({ raw: raw.trim(), rowIndex, columns: rowColumns(raw) }))
.filter((row) => row.raw);
}
function searchableText(candidate, columns = []) {
if (Array.isArray(candidate)) return candidate.join(' ');
if (candidate && Array.isArray(candidate.columns)) {
return columns.length ? columns.map((index) => candidate.columns[index] || '').join(' ') : candidate.columns.join(' ');
}
return String(candidate ?? '');
}
function classifyCandidate(candidate, keyword, columns = []) {
const haystack = normalizeText(searchableText(candidate, columns));
const needle = normalizeText(keyword);
const tokens = tokenizeKeyword(keyword);
const exact = Array.isArray(candidate?.columns)
&& candidate.columns.some((value) => normalizeText(value) === needle && needle);
const contiguous = Boolean(needle && haystack.includes(needle));
const tokenMatch = Boolean(tokens.length && tokens.every((token) => haystack.includes(token)));
return {
candidate,
exact,
contiguous,
tokenMatch,
score: exact ? 300 : (contiguous ? 200 : (tokenMatch ? 100 : 0))
};
}
function resolveUniqueSelectBoxRow(serialized, keyword, options = {}) {
const rows = Array.isArray(serialized) ? serialized : parseSelectBoxRows(serialized);
const columns = Array.isArray(options.searchableColumns) ? options.searchableColumns : [];
const classified = rows.map((row) => classifyCandidate(row, keyword, columns));
const choose = (rule, list) => {
if (list.length !== 1) return null;
return {
ok: true,
row: list[0].candidate,
rule,
exact_count: classified.filter((item) => item.exact).length,
contains_count: classified.filter((item) => item.contiguous).length,
token_match_count: classified.filter((item) => item.tokenMatch).length,
candidate_count: list.length
};
};
const exact = classified.filter((item) => item.exact);
const contiguous = classified.filter((item) => item.contiguous);
const token = classified.filter((item) => item.tokenMatch);
const match = choose('exact', exact) || choose('unique_contains', contiguous) || choose('unique_tokenized_contains', token);
if (match) return match;
const candidateCount = exact.length > 1 ? exact.length : (contiguous.length > 1 ? contiguous.length : token.length);
return {
ok: false,
row: null,
rule: exact.length > 1 ? 'ambiguous_exact' : (contiguous.length > 1 ? 'ambiguous_contains' : (token.length > 1 ? 'ambiguous_tokenized_contains' : 'not_found')),
exact_count: exact.length,
contains_count: contiguous.length,
token_match_count: token.length,
candidate_count: candidateCount,
blocker: `${options.label || 'lookup'}: expected exactly one deterministic match, found ${candidateCount}`
};
}
function resolveUniqueTextCandidate(candidates, keyword, options = {}) {
const source = Array.isArray(candidates) ? candidates : [];
const wrapped = source.map((candidate) => ({
...classifyCandidate({ columns: [candidate.text || candidate.label || ''] }, keyword, [0]),
candidate
}));
const exact = wrapped.filter((item) => normalizeText(item.candidate.label || item.candidate.text || '') === normalizeText(keyword));
const contiguous = wrapped.filter((item) => item.contiguous);
const token = wrapped.filter((item) => item.tokenMatch);
const selected = exact.length === 1
? { list: exact, rule: 'exact' }
: (contiguous.length === 1
? { list: contiguous, rule: 'unique_contains' }
: (token.length === 1 ? { list: token, rule: 'unique_tokenized_contains' } : null));
if (selected) {
return {
ok: true,
candidate: selected.list[0].candidate,
rule: selected.rule,
exact_count: exact.length,
contains_count: contiguous.length,
token_match_count: token.length,
candidate_count: selected.list.length
};
}
const count = exact.length > 1 ? exact.length : (contiguous.length > 1 ? contiguous.length : token.length);
return {
ok: false,
candidate: null,
rule: exact.length > 1 ? 'ambiguous_exact' : (contiguous.length > 1 ? 'ambiguous_contains' : (token.length > 1 ? 'ambiguous_tokenized_contains' : 'not_found')),
exact_count: exact.length,
contains_count: contiguous.length,
token_match_count: token.length,
candidate_count: count,
blocker: `${options.label || 'lookup'}: expected exactly one deterministic match, found ${count}`
};
}
function canonicalDate(value) {
const match = String(value ?? '').trim().match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})$/);
if (!match) return '';
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const date = new Date(Date.UTC(year, month - 1, day));
if (date.getUTCFullYear() !== year || date.getUTCMonth() + 1 !== month || date.getUTCDate() !== day) return '';
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
function erpDate(value) {
const canonical = canonicalDate(value);
if (!canonical) return '';
const [year, month, day] = canonical.split('-');
return `${year}-${Number(month)}-${Number(day)}`;
}
function dateNumber(value) {
const canonical = canonicalDate(value);
if (!canonical) return NaN;
return Date.parse(`${canonical}T00:00:00Z`);
}
function dateInRange(value, start, end) {
const current = dateNumber(value);
const from = dateNumber(start);
const to = dateNumber(end);
return Number.isFinite(current) && Number.isFinite(from) && Number.isFinite(to) && current >= from && current <= to;
}
function cycleShortcutValues(pattern, weekdays = []) {
const value = String(pattern || '').trim().toLowerCase();
if (value === 'daily' || value === 'all' || value === '天天') return ['All'];
if (value === 'odd_days' || value === 'odd' || value === '单日') return ['d1d', 'd3d', 'd5d', 'd7d', 'd9d'];
if (value === 'even_days' || value === 'even' || value === '双日') return ['d2d', 'd4d', 'd6d', 'd8d', 'd0d'];
if (value === 'weekly' || value === 'weekdays' || value === '星期') {
return [...new Set((Array.isArray(weekdays) ? weekdays : [weekdays])
.map((day) => Number(day))
.filter((day) => day >= 1 && day <= 7)
.map((day) => String(day === 7 ? 0 : day)))];
}
return [];
}
function uniqueCanonicalDates(values = []) {
return [...new Set((Array.isArray(values) ? values : [])
.map(canonicalDate)
.filter(Boolean))];
}
function addCanonicalDays(value, days) {
const canonical = canonicalDate(value);
if (!canonical) return '';
const [year, month, day] = canonical.split('-').map(Number);
const date = new Date(Date.UTC(year, month - 1, day + Number(days || 0)));
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`;
}
function weekdayOneToSeven(value) {
const canonical = canonicalDate(value);
if (!canonical) return 0;
const day = new Date(`${canonical}T00:00:00Z`).getUTCDay();
return day === 0 ? 7 : day;
}
function expandCycleDates(data = {}) {
const explicitDates = uniqueCanonicalDates(data.departure_dates);
const recurrence = data.recurrence || {};
const pattern = String(recurrence.pattern || data.cycle || '').trim().toLowerCase();
if (explicitDates.length) return explicitDates;
const start = canonicalDate(recurrence.start_date);
const end = canonicalDate(recurrence.end_date);
if (!start || !end || dateNumber(start) > dateNumber(end)) return [];
const weekdays = new Set((Array.isArray(recurrence.weekday) ? recurrence.weekday : [])
.map((value) => Number(value))
.filter((value) => value >= 1 && value <= 7));
const selected = [];
for (let cursor = start; dateNumber(cursor) <= dateNumber(end); cursor = addCanonicalDays(cursor, 1)) {
const [, , calendarDay] = cursor.split('-').map(Number);
const weekday = weekdayOneToSeven(cursor);
const keep = pattern === 'daily'
? true
: pattern === 'weekly'
? weekdays.has(weekday)
: pattern === 'odd_days'
? calendarDay % 2 === 1
: pattern === 'even_days'
? calendarDay % 2 === 0
: false;
if (keep) selected.push(cursor);
}
return selected;
}
function cycleSelectionPlan(data = {}) {
const explicitDates = uniqueCanonicalDates(data.departure_dates);
if (explicitDates.length) {
return {
ok: true,
mode: 'explicit_dates',
explicit_dates: explicitDates,
expanded_dates: explicitDates,
shortcut_values: []
};
}
const recurrence = data.recurrence || {};
const shortcutValues = cycleShortcutValues(recurrence.pattern || data.cycle, recurrence.weekday || []);
if (!shortcutValues.length) {
return {
ok: false,
mode: 'invalid',
explicit_dates: [],
expanded_dates: [],
shortcut_values: [],
blocker: '发团周期未能映射到 ERP 的日期枚举或快捷标签。'
};
}
return {
ok: true,
mode: 'shortcut',
explicit_dates: [],
expanded_dates: expandCycleDates(data),
shortcut_values: shortcutValues
};
}
return {
cycleSelectionPlan,
canonicalDate,
chineseOrLatinSegments,
classifyCandidate,
cycleShortcutValues,
dateInRange,
dateNumber,
erpDate,
expandCycleDates,
normalizeText,
parseSelectBoxRows,
resolveUniqueSelectBoxRow,
resolveUniqueTextCandidate,
tokenizeKeyword
};
}));