feat: automate extension updates across Windows hosts
This commit is contained in:
@@ -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 写入。
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -6118,7 +6118,7 @@
|
||||
}
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
version: '0.5.166',
|
||||
version: '0.5.167',
|
||||
resolveNativeListSearchValues,
|
||||
lookupKeywordMatchesText,
|
||||
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "联泰下单助手",
|
||||
"version": "0.5.166",
|
||||
"version": "0.5.167",
|
||||
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
|
||||
@@ -944,7 +944,7 @@
|
||||
|
||||
window.LTJTOrderAssistant = {
|
||||
...(window.LTJTOrderAssistant || {}),
|
||||
version: '0.5.166',
|
||||
version: '0.5.167',
|
||||
openTeamBatchForm,
|
||||
pingTeamBatchFrame,
|
||||
preflightTeamBatchNative,
|
||||
|
||||
Reference in New Issue
Block a user