feat: automate extension updates across Windows hosts

This commit is contained in:
inman
2026-09-03 15:54:22 +08:00
parent 500034bb63
commit f08aac0c9e
36 changed files with 2996 additions and 71 deletions

View File

@@ -11,9 +11,15 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
当前源码版本为 `0.5.166`。版本化 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.167`。版本化 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.166 当前重点:
0.5.167 当前重点:
- 插件 PING 会返回当前运行任务、持久化写入边界和待回查结果汇总后的 `extension_update.safe`。只有内存与持久化状态都完全空闲,服务端才允许同一 ECS 实例进入文件部署。
- 中央服务完成共享目录替换后,平台通过受控消息请求后台 `chrome.runtime.reload()`,并刷新平台页以重新注入 content script执行中、已跨写边界或待回查状态会拒绝重载。
- 这是自动更新协议的引导版本。每个 Windows Server/Chrome profile 仍需最后一次人工把插件加载到统一的 `C:\ProgramData\LTJT\chrome-extension\ltjt-order-assistant`;后续版本才由现有服务配合阿里云 ECS 云助手更新,不新增 LTJT 常驻更新器。
继续保留 0.5.166 的自适应等待:
- 散拼新增计划进入计划表后立即检查“新增计划”按钮;若同路径 iframe 尚在渲染,则每 100ms 重新取得当前 document 并检查按钮是否已连接且可用,出现后立即继续,最长 15 秒后才在写前阻断。快速页面没有固定等待,也不会自动重试任何 ERP 写入。

View File

@@ -917,6 +917,64 @@ async function hasActiveErpExecution() {
));
}
async function extensionUpdateSafetyStatus() {
if (runningTasks.size > 0) {
return {
ok: true,
safe: false,
reason: 'running_task',
active_task_count: runningTasks.size
};
}
const saved = await chrome.storage.local.get(['businessTaskExecutions', 'businessTaskResults']);
const executions = Object.values(saved.businessTaskExecutions || {});
if (executions.some((execution) => ['running', 'write_started', 'submitted', 'uncertain'].includes(String(execution?.state || '')))) {
return { ok: true, safe: false, reason: 'persisted_execution_active', active_task_count: 0 };
}
const results = Object.values(saved.businessTaskResults || {});
if (results.some((result) => /^(?:saved_unverified|execution_uncertain|reconciliation_pending)$/.test(String(result?.status || '')))) {
return { ok: true, safe: false, reason: 'reconciliation_pending', active_task_count: 0 };
}
return { ok: true, safe: true, reason: 'idle', active_task_count: 0 };
}
async function applyPreparedExtensionUpdate(targetVersion) {
const safety = await extensionUpdateSafetyStatus();
if (!safety.safe) {
return {
ok: false,
accepted: false,
status: 'update_reload_blocked',
message: '插件仍有执行中或待回查任务,暂不重载。',
update_safety: safety
};
}
const currentVersion = String(chrome.runtime.getManifest().version || '');
if (!/^\d+\.\d+\.\d+(?:\.\d+)?$/.test(String(targetVersion || ''))) {
return { ok: false, accepted: false, status: 'update_version_invalid', message: '目标插件版本无效。' };
}
const currentParts = currentVersion.split('.').map(Number);
const targetParts = String(targetVersion).split('.').map(Number);
let comparison = 0;
for (let index = 0; index < Math.max(currentParts.length, targetParts.length); index += 1) {
const difference = (currentParts[index] || 0) - (targetParts[index] || 0);
if (!difference) continue;
comparison = difference > 0 ? 1 : -1;
break;
}
if (comparison >= 0) {
return { ok: true, accepted: false, status: 'already_current', version: currentVersion };
}
setTimeout(() => chrome.runtime.reload(), 250);
return {
ok: true,
accepted: true,
status: 'update_reload_scheduled',
current_version: currentVersion,
target_version: String(targetVersion)
};
}
async function findExistingErpTabForKeepalive({ allowLoading = false } = {}) {
await requireErpHostPermission();
const tabs = await chrome.tabs.query({ url: `${ERP_ORIGIN}/*` });
@@ -3757,6 +3815,28 @@ chrome.runtime.onConnect.addListener((port) => {
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message?.type === 'LTJT_EXTENSION_UPDATE_STATUS') {
extensionUpdateSafetyStatus()
.then((result) => sendResponse(result))
.catch((error) => sendResponse({
ok: false,
safe: false,
reason: 'status_failed',
message: error.message || String(error)
}));
return true;
}
if (message?.type === 'LTJT_APPLY_EXTENSION_UPDATE') {
applyPreparedExtensionUpdate(message.target_version || '')
.then((result) => sendResponse(result))
.catch((error) => sendResponse({
ok: false,
accepted: false,
status: 'update_reload_failed',
message: error.message || String(error)
}));
return true;
}
if (message?.type === 'LTJT_ERP_SESSION_STATUS') {
readErpSessionStatus(message.expected_erp_account || '')
.then((result) => sendResponse(result))

View File

@@ -60,12 +60,21 @@ async function bridgePayload(extra = {}, expectedErpAccount = '') {
} catch (error) {
erpSession = { ...erpSession, message: error.message || String(error) };
}
let extensionUpdate = { ok: false, safe: false, reason: 'status_unavailable' };
try {
extensionUpdate = await chrome.runtime.sendMessage({
type: 'LTJT_EXTENSION_UPDATE_STATUS'
}) || extensionUpdate;
} catch (error) {
extensionUpdate = { ...extensionUpdate, message: error.message || String(error) };
}
return {
ok: true,
extension: 'ltjt-order-assistant',
version: chrome.runtime.getManifest().version,
bridge_installed_at: window.__LTJT_ORDER_ASSISTANT_BRIDGE_INSTALLED_AT__ || '',
erp_session: erpSession,
extension_update: extensionUpdate,
...(await getAutomationState()),
...extra
};
@@ -273,6 +282,19 @@ const bridgeHandler = async (event) => {
postReply(requestId, 'PONG', await bridgePayload({}, message.payload?.expected_erp_account));
return;
}
if (message.type === 'APPLY_EXTENSION_UPDATE') {
const result = await chrome.runtime.sendMessage({
type: 'LTJT_APPLY_EXTENSION_UPDATE',
target_version: String(message.payload?.target_version || '')
});
postReply(requestId, 'EXTENSION_UPDATE_APPLYING', result || {
ok: false,
accepted: false,
status: 'update_reload_failed',
message: '插件后台未返回重载结果。'
});
return;
}
if (message.type === 'CREATE_TASK') {
const task = await createTask(message.payload || {});
postReply(requestId, 'TASK_CREATED', {

View File

@@ -6118,7 +6118,7 @@
}
window.LTJTOrderAssistant = {
version: '0.5.166',
version: '0.5.167',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "联泰下单助手",
"version": "0.5.166",
"version": "0.5.167",
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
"permissions": [
"activeTab",

View File

@@ -944,7 +944,7 @@
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
version: '0.5.166',
version: '0.5.167',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,