fix: use native ERP product search
This commit is contained in:
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 登录身份和任务归属账号三者一致时,任务才可领取或回传结果。
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user