fix: use native ERP product search

This commit is contained in:
inman committed 2026-09-02 19:29:04 +08:00
1 parent a618d3dd9f
commit b5c727b6f9
20 files changed
+289 -58

No files matched your search

@@ -11,9 +11,11 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
当前源码版本为 `0.5.164`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
当前源码版本为 `0.5.165`。版本化 ZIP、文件哈希和 Skill/DOCX 基线见 [`../../dist/release-manifest.json`](../../dist/release-manifest.json)。旧版本实现流水已冻结在 [`../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md`](../../archive/project-history/2026-08-16/chrome-extension-README.pre-governance.md)。
0.5.164 当前重点:
0.5.165 当前重点:
- 散拼新增计划和独立团批量下单在已加载候选无法唯一匹配时,会真正触发 `S_chanpinming` 的原生非空检索并等待 Ajax 返回,再对产品名/时长做本地唯一匹配。完整产品名原样查询;只有尾部 `10D`、`8天` 等数字时长会先剥离。最后仍保留一次空条件兼容重载,零候选或多候选继续在写前阻断。
- 平台会把当前员工账号绑定的 ERP 账号随只读 PING 交给扩展核验;扩展只返回是否匹配,不回传页面中的其他账号文本。
- 只有平台确认的唯一在线云电脑、匹配的 ERP 登录身份和任务归属账号三者一致时,任务才可领取或回传结果。
+33 -19
View File
@@ -920,6 +920,15 @@
return { matched: false, rule: 'product_lookup_helper_missing' };
}
function nativeProductSearchQueries(query) {
const helper = window.LTJTProductLookup;
if (typeof helper?.nativeProductSearchQueries === 'function') {
return helper.nativeProductSearchQueries(query);
}
const keyword = String(query || '').trim();
return keyword ? [keyword, ''] : [''];
}
function parseErpResponseText(value) {
return String(value || '')
.replace(/<br\s*\/?>/gi, '\n')
@@ -1117,17 +1126,16 @@
.join('|');
}
async function reloadProductsForLocalMatch(scopeDocument, productSearchInput, keyword) {
async function reloadProductsForLocalMatch(scopeDocument, productSearchInput, keyword, nativeQuery = '') {
const previousSignature = productCandidateSignature(scopeDocument);
const ownerWindow = scopeDocument.defaultView || window;
const jq = ownerWindow.jQuery || ownerWindow.$;
const startedAt = Date.now();
// The ERP endpoint can search only a contiguous product-name fragment.
// An input such as “江西老挝10D” is a business shorthand composed of a
// product name and a duration; sending it to S_chanpinming makes the ERP
// SQL LIKE filter return zero rows. Reload with an empty native query,
// then resolve the shorthand against the returned candidate fields.
setDomField(scopeDocument, 'S_chanpinming', '', { optional: true });
// Dispatch the same blur-driven AjaxLoadData(true) flow used by the ERP
// form. The shared lookup helper removes only a trailing numeric duration
// shorthand; complete product names, including Chinese words such as
// “八天”, are sent unchanged.
setDomField(scopeDocument, 'S_chanpinming', nativeQuery, { optional: true });
const refreshed = await waitForCondition(() => {
const currentSignature = productCandidateSignature(scopeDocument);
const active = jq && Number.isFinite(Number(jq.active)) ? Number(jq.active) : 0;
@@ -1139,8 +1147,8 @@
}, 8000, 200);
productSearchInput.value = String(keyword || '');
return {
strategy: 'native_empty_then_local',
native_query: '',
strategy: nativeQuery ? 'native_query_then_local' : 'native_empty_then_local',
native_query: nativeQuery,
previous_candidate_count: previousSignature ? previousSignature.split('|').length : 0,
refreshed: Boolean(refreshed),
candidate_count: scopeDocument.querySelectorAll('input[name="cp_id"], input[name="cpid"]').length
@@ -3898,20 +3906,24 @@
setDomField(formDocument, 'Riqi2', dateEnd);
setDomField(formDocument, 'jihuashu', data.planned_capacity);
// The ERP search input fires AjaxLoadData(true) on blur. A shorthand such
// as "老挝广东8D" is intentionally matched against the loaded product
// name/duration fields first. If the current table cannot resolve it,
// reload the broad ERP candidate set with an empty native query and keep
// the shorthand matching local; never send a compound name+duration token
// to the ERP's contiguous S_chanpinming LIKE filter.
// Match the currently loaded candidates first. If they do not contain a
// unique result, invoke the ERP's native non-empty product-name search and
// match the returned name/duration fields locally. A trailing numeric
// duration shorthand is excluded from the native LIKE query; a bounded
// empty-query reload remains the final compatibility fallback.
const productKeyword = data.product.keyword || data.product.name;
const productSearchInput = formDocument.querySelector('#S_chanpinming, [name="S_chanpinming"]');
if (productSearchInput) productSearchInput.value = String(productKeyword || '');
let product = selectProductRadio(formDocument, data);
let productSearch = { strategy: 'loaded_candidates', native_query: null, refreshed: false };
const productSearchAttempts = [];
if (!product.ok && !data.product?.cpid && !data.product?.id && productSearchInput) {
productSearch = await reloadProductsForLocalMatch(formDocument, productSearchInput, productKeyword);
product = selectProductRadio(formDocument, data);
for (const nativeQuery of nativeProductSearchQueries(productKeyword)) {
productSearch = await reloadProductsForLocalMatch(formDocument, productSearchInput, productKeyword, nativeQuery);
productSearchAttempts.push(productSearch);
product = selectProductRadio(formDocument, data);
if (product.ok) break;
}
}
lookupChecks.push({
name: 'product',
@@ -3923,7 +3935,9 @@
search_strategy: productSearch.strategy,
native_query: productSearch.native_query,
refreshed: productSearch.refreshed,
refreshed_candidate_count: productSearch.candidate_count
refreshed_candidate_count: productSearch.candidate_count,
search_attempt_count: productSearchAttempts.length,
search_attempt_strategies: productSearchAttempts.map((attempt) => attempt.strategy)
});
if (!product.ok) {
closeDialogDocument(formDocument);
@@ -6077,7 +6091,7 @@
}
window.LTJTOrderAssistant = {
version: '0.5.164',
version: '0.5.165',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "联泰下单助手",
"version": "0.5.164",
"version": "0.5.165",
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
"permissions": [
"activeTab",
@@ -62,6 +62,21 @@
};
}
function nativeProductSearchQueries(query) {
const keyword = collapseWhitespace(query);
if (!keyword) return [''];
// S_chanpinming is a contiguous product-name LIKE filter; duration is a
// separate ERP field. Preserve a complete product name verbatim, but trim
// a trailing Arabic duration shorthand before issuing the native query.
// Keep one bounded empty-query fallback for legacy shorthand whose name
// fragments are not contiguous in the ERP product name.
const productNameQuery = keyword
.replace(/\s*\d+(?:\.\d+)?\s*(?:D|天)(?:\s*\d+(?:\.\d+)?\s*(?:N|晚|夜))?\s*$/iu, '')
.trim();
return productNameQuery ? [productNameQuery, ''] : [''];
}
function productCandidateFromRow(row, input = null) {
const label = productLabelFromRow(row);
const parts = productNameAndDuration(label);
@@ -142,6 +157,7 @@
isOrderedSubsequence,
productLabelFromRow,
productNameAndDuration,
nativeProductSearchQueries,
productCandidateFromRow,
productQueryTokens,
productQueryMatch,
@@ -279,14 +279,25 @@
};
}
async function reloadProductsForLocalMatch(form, input, keyword) {
function nativeProductSearchQueries(keyword) {
if (typeof productLookup.nativeProductSearchQueries === 'function') {
return productLookup.nativeProductSearchQueries(keyword);
}
const normalized = String(keyword || '').trim();
return normalized ? [normalized, ''] : [''];
}
async function reloadProductsForLocalMatch(form, input, keyword, nativeQuery = '') {
const previousSignature = productCandidates(form)
.map((candidate) => `${candidate.cpid || candidate.input?.value || ''}:${candidate.label || ''}`)
.join('|');
const ownerWindow = form.ownerDocument.defaultView || window;
const jq = ownerWindow.jQuery || ownerWindow.$;
const startedAt = Date.now();
setField(form, 'S_chanpinming', '');
// Trigger the form's native blur-driven AjaxLoadData(true) search. The
// shared helper preserves complete product names and removes only a
// trailing numeric duration shorthand from this contiguous LIKE query.
setField(form, 'S_chanpinming', nativeQuery);
const candidates = await waitFor(() => {
const current = productCandidates(form);
const signature = current
@@ -302,8 +313,8 @@
input.value = String(keyword || '');
return {
candidates,
strategy: 'native_empty_then_local',
native_query: '',
strategy: nativeQuery ? 'native_query_then_local' : 'native_empty_then_local',
native_query: nativeQuery,
previous_candidate_count: previousSignature ? previousSignature.split('|').length : 0,
refreshed: candidates.length > 0 && candidates
.map((candidate) => `${candidate.cpid || candidate.input?.value || ''}:${candidate.label || ''}`)
@@ -320,14 +331,24 @@
input.value = keyword;
let candidates = await waitForProductList(form);
let search = { strategy: 'loaded_candidates', native_query: null, refreshed: false };
const searchAttempts = [];
let result = resolveProductCandidates(candidates, keyword);
if (!result.ok) {
if (typeof ownerWindow.AjaxLoadData !== 'function') return { ...result, blocker: result.blocker || 'ERP 产品 AjaxLoadData 函数不存在。' };
search = await reloadProductsForLocalMatch(form, input, keyword);
candidates = search.candidates;
result = resolveProductCandidates(candidates, keyword);
for (const nativeQuery of nativeProductSearchQueries(keyword)) {
search = await reloadProductsForLocalMatch(form, input, keyword, nativeQuery);
searchAttempts.push(search);
candidates = search.candidates;
result = resolveProductCandidates(candidates, keyword);
if (result.ok) break;
}
}
if (!result.ok) return { ...result, ...search };
if (!result.ok) return {
...result,
...search,
search_attempt_count: searchAttempts.length,
search_attempt_strategies: searchAttempts.map((attempt) => attempt.strategy)
};
const selected = result.candidate.input;
selected.click();
selected.checked = true;
@@ -342,6 +363,8 @@
search_strategy: search.strategy,
native_query: search.native_query,
refreshed: search.refreshed,
search_attempt_count: searchAttempts.length,
search_attempt_strategies: searchAttempts.map((attempt) => attempt.strategy),
selected_label_length: String(result.candidate.label || '').length,
selected_value_present: Boolean(String(selected.value || '').trim()),
group_prefix_present: Boolean(fieldValue(form, 'tuanxuhao1').trim()),
@@ -613,7 +636,9 @@
search_strategy: productResult.search_strategy || 'unknown',
native_query: productResult.native_query,
refreshed: productResult.refreshed === true,
candidate_count: productResult.candidate_count || 0
candidate_count: productResult.candidate_count || 0,
search_attempt_count: productResult.search_attempt_count || 0,
search_attempt_strategies: productResult.search_attempt_strategies || []
});
if (!productResult.ok) blockers.push(productResult.blocker || '业务产品未能唯一匹配。');
const requestedOrderSuffix = String(data.order_number?.suffix || '').trim();
@@ -919,7 +944,7 @@
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
version: '0.5.164',
version: '0.5.165',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,