diff --git a/LianSyn-platform/styles.css b/LianSyn-platform/styles.css
index 17d35fb..a8b5e96 100644
--- a/LianSyn-platform/styles.css
+++ b/LianSyn-platform/styles.css
@@ -638,38 +638,6 @@ textarea {
color: var(--ink) !important;
}
-.account-extension-release {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-wrap: wrap;
- gap: 10px 16px;
- padding: 12px;
- border: 1px solid var(--line);
- border-radius: 8px;
- background: var(--surface-muted);
-}
-
-.account-extension-release h3 {
- font-size: 14px;
-}
-
-.account-extension-release .actions {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.account-extension-release input[type='file'] {
- max-width: 280px;
- font-size: 11px;
-}
-
-.account-extension-release .channel-message {
- flex-basis: 100%;
- margin: 0;
-}
-
.account-authorization-panel {
display: grid;
gap: 12px;
diff --git a/agent设计规范/businesses/shared_plan_create.md b/agent设计规范/businesses/shared_plan_create.md
index 68c51cd..d56c988 100644
--- a/agent设计规范/businesses/shared_plan_create.md
+++ b/agent设计规范/businesses/shared_plan_create.md
@@ -75,7 +75,7 @@
当前页面为已登录的 `https://lwlt.hisy.cc/System/Mainlt.asp`。插件按以下顺序操作:
-1. 进入“散拼团计划表”后立即检查“新增计划”按钮;若 ERP 主 iframe 已进入 `/System/Business/plan.asp` 但按钮仍在渲染,则持续重新取得当前 iframe document 并按条件短轮询,按钮一旦存在且可用便立即点击,不使用固定等待。只有在 15 秒总超时后仍不可用才在写前阻断,然后打开 `/System/Business/plan_add.asp`。
+1. 进入“散拼团计划表”,点击“新增计划”,打开 `/System/Business/plan_add.asp`。
2. 填写 `Riqi1=2026-09-01`、`Riqi2=2026-09-30`。
3. 先在散拼团产品列表的产品名称/天数单元格中按 `广东衡阳6D` 做连续或有序唯一匹配并点选 `cp_id`。若当前候选无法解析,调用 `S_chanpinming` 的原生非空检索:完整产品名原样搜索,`10D`/`8天` 等尾部数字时长先剥离后只搜索产品名;再对返回候选本地唯一匹配,必要时以空条件做一次兼容重载。点选后的 `set_th` 必须自动带入团号前缀/后缀。
4. 填写 `jihuashu=40`。
@@ -109,7 +109,6 @@
- 日期枚举无法完整覆盖周期日期,或同一日期会提交多个勾选值;
- 计划容量或房型总数为 0,或已提供的可选字段未能正确落到 ERP 控件;
- ERP 默认跟团人/销售人为空;
-- 散拼团计划表在 15 秒条件等待后仍未出现可用的“新增计划”按钮;
- 页面自身校验弹窗、登录失效、权限错误、响应不明确或任一返回团号回查未命中。
## 3. Skill 适配
diff --git a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md
index 7a29679..5cee395 100644
--- a/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md
+++ b/agent设计规范/test-fixtures/lwlt-lifecycle/release-gate.md
@@ -2,15 +2,12 @@
## 当前基线
-- Chrome 插件:`0.5.167`
+- Chrome 插件:`0.5.165`
- Agent Prompt:`ltjt-agent-prompt-v1.8-independent-headcount-categories`
- 生命周期契约:`ltjt-lifecycle-v2.9-roster-leader-contact-2026-08`
- 五个 Skill:`0.5.125`;运营 DOCX:`0.5.125`
- 发布状态:核心窄能力已有真实证据;一笔名单附件 Program-only 任务已把 25 行及唯一领队联系人正确保存到 ERP,后续只读逐字段核验全等。两种写后逐行读取方式都曾在真实成功写入后产生假阴性;名单在全部写前门禁通过后,以父表单 ERP 明确成功响应作为终结证据,不再执行写后逐行回查或自动名单对账。`0.5.155` 规定确认覆盖的指定序号同值也写;`0.5.156` 把原生游客位不足统一映射为包含名单人数和 ERP 实际上限的业务提示,原技术 blocker 仅留技术详情。`0.5.157` 新增散拼母团“整团游客信息”的 `shared_plan + visitor-list + tid-only` 窄分支,静态契约与回归已通过,仍待授权后的新版运行态只读复测。`0.5.158` 把正式控制面 `https://lwlt.nianxx.cn` 同步加入 Popup、后台、Manifest 权限和 content script 精确白名单;其他 HTTPS origin 仍保持阻断。`0.5.159` 修复 MV3 后台恢复被中断后持久化 `running` 永久阻断插件派发的问题:仅在内存恢复 Promise 已不存在时收敛为 `interrupted`,真实活动恢复仍保持阻断,下一次成功只读保活把状态归零为 `idle`。`0.5.160` 为独立团单个下单写前漂移增加仅字段名诊断,并把数字后缀动态应收字段纳入保护。`0.5.161` 等待客户、跟单人和销售人原生 lookup 数据就绪后才开始单团预检;生产反馈证明仅检查 lookup 数据仍不足。`0.5.162` 进一步要求页面 jQuery Ajax 在预检前归零,并在 `GetProduct` 后等待原生 `ajaxStop` 再应用最终业务字段。`0.5.163` 按生产操作方明确要求恢复 `0.5.156` 的实际校验范围:核心订单字段继续写前阻断,带编号的动态 `ys_*` 应收行不再纳入保护哈希,允许 ERP 原生回调在预检后调整这些字段。`0.5.164` 新增平台账号、AgentBus 渠道、唯一云电脑 worker 与 ERP 登录身份四方一致门禁;旧未绑定渠道和历史未归属 AgentBus 任务默认不执行。`0.5.165` 让散拼新增计划和独立团批量下单在本地候选未命中时先使用表单原生非空产品检索,实测两类表单均能由完整关键词唯一返回目标产品;本轮未勾选或保存,自动选择与写入仍由原门禁保护。新增独立团房型/人数映射仍待实写;未验证宽能力继续失败关闭。
-- `0.5.166` 在散拼母团列表入口加入立即检查、100ms 条件轮询和 15 秒总超时;每次探测都重新取得当前主 iframe document,按钮一旦可用便立即继续。该等待只发生在点击前,不固定拖慢快速页面,也不触发业务写入重试。
-- `0.5.167` 新增中央服务编排的私有 OSS 插件发布、账号到 ECS 实例绑定、整机空闲门禁、阿里云 ECS 云助手一次性 PowerShell 部署、SHA-256/Manifest 双重校验、目录替换回滚和浏览器安全重载协议。任何同机在线账号仍在执行、已跨写边界或待回查时都不更新;只要浏览器版本低于服务端活动版本,任务领取在等待、部署、待重载、配置缺失或最终失败时都保持关闭,直到目标版本心跳验版。该版本是自动更新引导版本,首次迁移到共享 ProgramData 目录仍需人工完成。
-
运行中的浏览器版本必须实时握手确认。当前制品文件名和 SHA-256 只看 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json),不从历史日志推断。
## 已证明的核心能力
diff --git a/archive/releases/2026-09-03/README.md b/archive/releases/2026-09-03/README.md
deleted file mode 100644
index 0ceedf9..0000000
--- a/archive/releases/2026-09-03/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# 2026-09-03 发布归档
-
-本目录只保存被新版本替代的历史发布物,不定义当前运行规则。当前制品文件名和 SHA-256 只以根目录 [`../../../dist/release-manifest.json`](../../../dist/release-manifest.json) 为准。
-
-- `release-manifest-0.5.165.json`:加入散拼母团列表入口自适应就绪等待前的历史清单快照。
-- `ltjt-order-assistant-0.5.165.zip`:被 `0.5.166` 的散拼母团“新增计划”按钮自适应条件等待替代;保留用于恢复和追溯。
-- `release-manifest-0.5.166.json`:加入服务端 OSS 发布、ECS 云助手部署和浏览器空闲重载协议前的历史清单快照。
-- `ltjt-order-assistant-0.5.166.zip`:被 `0.5.167` 的中央服务编排更新能力替代;仍保留自适应就绪等待修复,可用于恢复和追溯。
diff --git a/archive/releases/2026-09-03/ltjt-order-assistant-0.5.166.zip b/archive/releases/2026-09-03/ltjt-order-assistant-0.5.166.zip
deleted file mode 100644
index 3dd375e..0000000
Binary files a/archive/releases/2026-09-03/ltjt-order-assistant-0.5.166.zip and /dev/null differ
diff --git a/archive/releases/2026-09-03/release-manifest-0.5.165.json b/archive/releases/2026-09-03/release-manifest-0.5.165.json
deleted file mode 100644
index bfdd98d..0000000
--- a/archive/releases/2026-09-03/release-manifest-0.5.165.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
- "manifest_version": 1,
- "generated_on": "2026-09-02",
- "baselines": {
- "chrome_extension": "0.5.165",
- "skills": "0.5.125",
- "business_instruction_docx": "0.5.125",
- "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
- },
- "artifacts": [
- {
- "kind": "chrome_extension",
- "version": "0.5.165",
- "path": "dist/ltjt-order-assistant-0.5.165.zip",
- "source": "chrome-extension/ltjt-order-assistant",
- "sha256": "14f3150ab26d99131e32321ddc805a85550438bc2810a1e20921f7922b25aaac"
- },
- {
- "kind": "skill",
- "name": "lwlt-arrangement",
- "version": "0.5.125",
- "path": "dist/lwlt-arrangement-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-arrangement",
- "sha256": "c729bebec84695b8642a6d31cb4dc0d963135b6c6620c2173be095826cd3976d"
- },
- {
- "kind": "skill",
- "name": "lwlt-confirmation",
- "version": "0.5.125",
- "path": "dist/lwlt-confirmation-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-confirmation",
- "sha256": "659f6aca7918a03a29fc78f0b4b938dfa9084ccdbb44ebd71f6399556cfa4609"
- },
- {
- "kind": "skill",
- "name": "lwlt-lifecycle",
- "version": "0.5.125",
- "path": "dist/lwlt-lifecycle-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-lifecycle",
- "sha256": "d964c0a9e482f82757fea25d4713f0c81feb2704486583dfa84f2336ecd930c0"
- },
- {
- "kind": "skill",
- "name": "lwlt-newbooking",
- "version": "0.5.125",
- "path": "dist/lwlt-newbooking-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-newbooking",
- "sha256": "bb610c50dd659fc172aabfa469b3028de98704c249cfd4890463deea0c88cd0c"
- },
- {
- "kind": "skill",
- "name": "lwlt-updating",
- "version": "0.5.125",
- "path": "dist/lwlt-updating-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-updating",
- "sha256": "c6f5bc9008e2f63d170f3f140140003d99411ce5232632c48b38891da1ceee33"
- },
- {
- "kind": "business_instruction_docx",
- "version": "0.5.125",
- "path": "dist/老挝联泰AI指令表-0.5.125.docx",
- "source": "agent设计规范/templates/business-input-templates.md",
- "builder": "tools/build_business_instruction_docx.py",
- "sha256": "a0c2fd5f808e3d3f2ac211a652a59f2e435d3824df49c51f763ca4beb7d9b97d",
- "source_sha256": "27600fe9bcb0de3898a99550428a9890dd350c701ba704fb3940ca966088dcd9",
- "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1"
- }
- ]
-}
diff --git a/archive/releases/2026-09-03/release-manifest-0.5.166.json b/archive/releases/2026-09-03/release-manifest-0.5.166.json
deleted file mode 100644
index a8fe92b..0000000
--- a/archive/releases/2026-09-03/release-manifest-0.5.166.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
- "manifest_version": 1,
- "generated_on": "2026-09-02",
- "baselines": {
- "chrome_extension": "0.5.166",
- "skills": "0.5.125",
- "business_instruction_docx": "0.5.125",
- "agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
- },
- "artifacts": [
- {
- "kind": "chrome_extension",
- "version": "0.5.166",
- "path": "dist/ltjt-order-assistant-0.5.166.zip",
- "source": "chrome-extension/ltjt-order-assistant",
- "sha256": "68aeda857f0bf596eed3c5012fb37c6292977935ff215ce1d133983a8dffe259"
- },
- {
- "kind": "skill",
- "name": "lwlt-arrangement",
- "version": "0.5.125",
- "path": "dist/lwlt-arrangement-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-arrangement",
- "sha256": "c729bebec84695b8642a6d31cb4dc0d963135b6c6620c2173be095826cd3976d"
- },
- {
- "kind": "skill",
- "name": "lwlt-confirmation",
- "version": "0.5.125",
- "path": "dist/lwlt-confirmation-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-confirmation",
- "sha256": "659f6aca7918a03a29fc78f0b4b938dfa9084ccdbb44ebd71f6399556cfa4609"
- },
- {
- "kind": "skill",
- "name": "lwlt-lifecycle",
- "version": "0.5.125",
- "path": "dist/lwlt-lifecycle-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-lifecycle",
- "sha256": "d964c0a9e482f82757fea25d4713f0c81feb2704486583dfa84f2336ecd930c0"
- },
- {
- "kind": "skill",
- "name": "lwlt-newbooking",
- "version": "0.5.125",
- "path": "dist/lwlt-newbooking-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-newbooking",
- "sha256": "bb610c50dd659fc172aabfa469b3028de98704c249cfd4890463deea0c88cd0c"
- },
- {
- "kind": "skill",
- "name": "lwlt-updating",
- "version": "0.5.125",
- "path": "dist/lwlt-updating-0.5.125.skill",
- "source": "agent设计规范/skills/lwlt-updating",
- "sha256": "c6f5bc9008e2f63d170f3f140140003d99411ce5232632c48b38891da1ceee33"
- },
- {
- "kind": "business_instruction_docx",
- "version": "0.5.125",
- "path": "dist/老挝联泰AI指令表-0.5.125.docx",
- "source": "agent设计规范/templates/business-input-templates.md",
- "builder": "tools/build_business_instruction_docx.py",
- "sha256": "a0c2fd5f808e3d3f2ac211a652a59f2e435d3824df49c51f763ca4beb7d9b97d",
- "source_sha256": "27600fe9bcb0de3898a99550428a9890dd350c701ba704fb3940ca966088dcd9",
- "builder_sha256": "c162884210da22e3b78368749b66f1f177df5e9fc6155f4fd9954bd516d187f1"
- }
- ]
-}
diff --git a/chrome-extension/ltjt-order-assistant/README.md b/chrome-extension/ltjt-order-assistant/README.md
index 234b64c..f09b077 100644
--- a/chrome-extension/ltjt-order-assistant/README.md
+++ b/chrome-extension/ltjt-order-assistant/README.md
@@ -11,17 +11,9 @@ Chrome Manifest V3 扩展,在用户已登录的 LTJT ERP 页面内执行经过
## 当前版本
-当前源码版本为 `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.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.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 写入。
+0.5.165 当前重点:
- 散拼新增计划和独立团批量下单在已加载候选无法唯一匹配时,会真正触发 `S_chanpinming` 的原生非空检索并等待 Ajax 返回,再对产品名/时长做本地唯一匹配。完整产品名原样查询;只有尾部 `10D`、`8天` 等数字时长会先剥离。最后仍保留一次空条件兼容重载,零候选或多候选继续在写前阻断。
diff --git a/chrome-extension/ltjt-order-assistant/background.js b/chrome-extension/ltjt-order-assistant/background.js
index ce60ae7..07b7dac 100644
--- a/chrome-extension/ltjt-order-assistant/background.js
+++ b/chrome-extension/ltjt-order-assistant/background.js
@@ -917,64 +917,6 @@ 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}/*` });
@@ -3815,28 +3757,6 @@ 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))
diff --git a/chrome-extension/ltjt-order-assistant/business-bridge.js b/chrome-extension/ltjt-order-assistant/business-bridge.js
index 2362216..c3873b3 100644
--- a/chrome-extension/ltjt-order-assistant/business-bridge.js
+++ b/chrome-extension/ltjt-order-assistant/business-bridge.js
@@ -60,21 +60,12 @@ 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
};
@@ -282,19 +273,6 @@ 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', {
diff --git a/chrome-extension/ltjt-order-assistant/inpage.js b/chrome-extension/ltjt-order-assistant/inpage.js
index da2ab9d..d0cd1a4 100644
--- a/chrome-extension/ltjt-order-assistant/inpage.js
+++ b/chrome-extension/ltjt-order-assistant/inpage.js
@@ -2,8 +2,6 @@
'use strict';
const SPLIT_PARENT_RESPONSE_REQUERY_DELAYS_MS = Object.freeze([0, 750, 2000]);
- const SPLIT_PARENT_ENTRY_READY_TIMEOUT_MS = 15000;
- const SPLIT_PARENT_ENTRY_READY_INTERVAL_MS = 100;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -1353,24 +1351,6 @@
return current;
}
- async function waitForSplitParentAddButton(initialListDocument) {
- return waitForCondition(() => {
- // The ERP can expose the destination path before replacing or finishing
- // the iframe document. Reacquire it on every probe so a stale same-path
- // document cannot make the first execution fail while the button renders.
- const candidate = mainIframe() ? mainIframeDocument() : initialListDocument;
- if (!candidate) return null;
- try {
- if (!/\/system\/business\/plan\.asp$/i.test(candidate.location.pathname)) return null;
- const addButton = candidate.querySelector('#NewBuildB, [name="NewBuildB"]');
- if (!addButton || addButton.isConnected === false || addButton.disabled === true) return null;
- return { document: candidate, addButton };
- } catch (error) {
- return null;
- }
- }, SPLIT_PARENT_ENTRY_READY_TIMEOUT_MS, SPLIT_PARENT_ENTRY_READY_INTERVAL_MS);
- }
-
function nativeListRequestMatches(settings = {}, scopeDocument) {
let requestUrl;
try {
@@ -3899,16 +3879,9 @@
S_chufariqi: dateStart,
S_chufarizhi: dateEnd
});
- const listReady = await waitForSplitParentAddButton(listDocument);
- if (!listReady) {
- return {
- status: 'split_parent_live_blocked',
- blockers: ['split parent add button not found after readiness timeout'],
- no_erp_write: true
- };
- }
- listDocument = listReady.document;
- listReady.addButton.click();
+ const addButton = listDocument.querySelector('#NewBuildB, [name="NewBuildB"]');
+ if (!addButton) return { status: 'split_parent_live_blocked', blockers: ['split parent add button not found'], no_erp_write: true };
+ addButton.click();
const formDocument = await waitForDocumentPath(/\/system\/business\/plan_add\.asp$/i, 30000);
if (!formDocument) return { status: 'split_parent_live_blocked', blockers: ['split parent form did not load'], no_erp_write: true };
const ready = await waitForCondition(() => {
@@ -6118,7 +6091,7 @@
}
window.LTJTOrderAssistant = {
- version: '0.5.167',
+ version: '0.5.165',
resolveNativeListSearchValues,
lookupKeywordMatchesText,
inspectLifecycleSearchCriteria: lifecycleSearchCriteria,
diff --git a/chrome-extension/ltjt-order-assistant/manifest.json b/chrome-extension/ltjt-order-assistant/manifest.json
index 78131b9..1ff1c19 100644
--- a/chrome-extension/ltjt-order-assistant/manifest.json
+++ b/chrome-extension/ltjt-order-assistant/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "联泰下单助手",
- "version": "0.5.167",
+ "version": "0.5.165",
"description": "在已登录 LTJT ERP 页面内规划并受控执行联泰 ERP 业务操作。",
"permissions": [
"activeTab",
diff --git a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js
index c7c6127..26784a1 100644
--- a/chrome-extension/ltjt-order-assistant/team-batch-inpage.js
+++ b/chrome-extension/ltjt-order-assistant/team-batch-inpage.js
@@ -944,7 +944,7 @@
window.LTJTOrderAssistant = {
...(window.LTJTOrderAssistant || {}),
- version: '0.5.167',
+ version: '0.5.165',
openTeamBatchForm,
pingTeamBatchFrame,
preflightTeamBatchNative,
diff --git a/control-plane/README.md b/control-plane/README.md
index 2d49219..d6d48b8 100644
--- a/control-plane/README.md
+++ b/control-plane/README.md
@@ -31,39 +31,6 @@
- `user_channels`、`tasks.channel_id` 和 `agentbus_deliveries` 共同保存入站归属、accepted 受理回执和最终 result 回执。回执以 `(channel_id, inbound_frame_id, delivery_kind)` 幂等,发送失败会重试,进程重启或 WebSocket 重连后仍会继续投递;因此不会因为超过原等待时长而丢掉最终回复。
- ERP 插件领取按任务 `assigned_user_id` 使用账户级数据库锁和 FIFO confirmed 队列:同一平台/ERP 账户在任意时刻最多一个 ERP execution,该账户的其他任务留在服务端等待;不同账户的活跃或待执行任务互不占用队列位置、可独立领取执行。管理员的组织级查看权限与执行权限完全分离:实时执行事件、插件领取、执行回执以及强制删除后的浏览器清理命令都只发送或接受任务 `assigned_user_id` 对应的登录账号,管理员不会因为能查看员工任务而收到或处理该员工的插件任务。已开始写入但结果不确定的任务只阻塞同一账户的后续领取,直到人工回查收敛或任务被明确强制删除。
-## Chrome 插件自动更新
-
-插件更新由本控制平面统一编排,不在 Windows Server 上增加单独的 LTJT 更新服务。中央 Node 服务把管理员批准的 ZIP 写入 OSS 私有对象,再通过阿里云 ECS 云助手向目标 Windows Server 下发一次性 PowerShell 命令。云助手只负责本次文件部署;版本判断、空闲门禁、重试、状态和验版均保存在控制平面与 PostgreSQL 中。
-
-更新链路为:
-
-```text
-管理员发布版本化 ZIP
- → 服务端校验 ZIP 边界、Manifest 身份/版本、必需文件和 SHA-256
- → 私有对象写入 OSS,并把该版本设为组织活动版本
- → 浏览器心跳上报当前版本与插件内存/持久化空闲证明
- → 服务端汇总同一 ECS 实例上全部账号、任务、执行尝试与待回查状态
- → 完全空闲后,以主机级数据库锁启动 ECS 云助手命令
- → Windows 下载短时、主机绑定的服务端地址并再次校验 SHA-256 与 Manifest
- → 新目录暂存,旧目录保留为 `.previous`,原子切换失败则回滚
- → 平台请求插件后台再次检查空闲状态并执行 `chrome.runtime.reload()`
- → 刷新平台页;目标版本的新心跳到达后标记 `verified`,再开放新 ERP 任务
-```
-
-不存在固定的业务等待时间:已经空闲的主机会立即开始;仍有 `accepted/running/write_started/submitted/uncertain/reconciliation_pending` 边界时持续等待真实状态收敛。更新处于等待、执行、待重载、配置缺失或最终失败时,页面和服务端领取接口都会阻止新的 ERP 写任务。单个主机版本最多自动尝试三次;失败信息经过 URL/令牌脱敏后才写入状态。
-
-多台、多账号 Windows Server 按以下方式配置:
-
-1. 每台 ECS Windows Server 必须安装并正常连接阿里云云助手。中央服务所在机器需要访问 OSS 与 ECS API;每台 Windows Server 需要能访问 `APP_ORIGIN` 的 HTTPS 插件下载接口。该链路不依赖 Google 服务。
-2. 每台 Windows Server 统一使用 `C:\ProgramData\LTJT\chrome-extension\ltjt-order-assistant`。同机的每个 Windows/Chrome profile 都只需在 `chrome://extensions` 中把这个相同目录“加载已解压的扩展程序”一次。
-3. `0.5.167` 是引导版本,包含安全状态与受控重载协议。现有 `0.5.166` 及更旧 profile 必须最后一次人工迁移到上述共享目录;旧代码无法凭服务端单方面获得新协议。完成全部 profile 引导前保持 `EXTENSION_AUTO_UPDATE_ENABLED=false`。
-4. 为中央服务配置仅允许目标 ECS 实例执行命令和读取调用结果的 RAM 身份,并填写 `ALIBABA_CLOUD_ACCESS_KEY_ID`、`ALIBABA_CLOUD_ACCESS_KEY_SECRET`,使用临时身份时同时填写 `ALIBABA_CLOUD_SECURITY_TOKEN`。不要复用宽权限 OSS 身份。
-5. 配置 OSS、`EXTENSION_UPDATE_OSS_KEY_PREFIX`、共享安装目录、包大小与命令超时,再启用 `EXTENSION_AUTO_UPDATE_ENABLED=true` 并重启控制平面。生产 `APP_ORIGIN` 必须是 Windows Server 可达的 HTTPS 地址。
-6. 管理员在 `/accounts` 给每个非管理员账号填写 ECS 地域 ID 与实例 ID。同一 Windows Server 上的多个账号填写完全相同的一组值,服务端会把它们合并成一个主机级空闲门禁和更新状态。
-7. 管理员在 `/accounts` 的“插件版本发布”区域选择版本化 ZIP。服务端只接受比当前活动版本更高的版本;同一版本不同哈希会被拒绝。发布后无需逐台登录,在线浏览器的正常心跳会启动更新并完成验版。
-
-更新命令只允许操作配置的 `ProgramData\LTJT` 子目录,不安装 CRX、不修改 Chrome 策略、不操纵交互式桌面。若某个 profile 长期离线,它不会阻塞在线 profile;再次启动时会从已更新的共享目录加载目标版本并在下一次心跳完成验证。发布、迁移和运行状态由迁移 `019_extension_host_updates` 中的 `extension_releases`、`extension_host_updates` 以及账号 ECS 映射保存。
-
## 任务级会话续接
- `agent_sessions` 将一个业务任务绑定到一个 Superagent 会话;`agent_session_messages` 加密保存每一轮用户/Agent 消息并使用独立幂等键。
@@ -174,13 +141,13 @@ npm run data:retention
npm run dev
```
-`npm run dev` 和 `npm start` 会先执行数据库迁移,再启动控制平面;直接运行 `control-plane/src/server.ts` 或构建后的 `server.js` 时,服务也会在启动前检查必需迁移 `019_extension_host_updates`,缺失时拒绝监听端口。`db:migrate` 和管理员初始化需要可连接的 PostgreSQL。开发机没有数据库时,可以运行 `npm run test:control-plane` 完成无数据库静态/健康烟测。
+`npm run dev` 和 `npm start` 会先执行数据库迁移,再启动控制平面;直接运行 `control-plane/src/server.ts` 或构建后的 `server.js` 时,服务也会在启动前检查必需迁移 `018_agentbus_account_workers`,缺失时拒绝监听端口。`db:migrate` 和管理员初始化需要可连接的 PostgreSQL。开发机没有数据库时,可以运行 `npm run test:control-plane` 完成无数据库静态/健康烟测。
`/health/ready` 同时检查 PostgreSQL 可用性和必需 schema 版本;迁移未完成时返回 503,并标明 `required_migration`,避免任务在数据库结构未升级时进入解析队列。
## 生产部署
-1. 复制 `.env.production.example` 为部署机受保护的 `.env.production`,填入 `DEPLOYMENT_REVISION`、PostgreSQL URL、`DATABASE_SCHEMA`、字段加密密钥、外部解析 Key 和 OSS 凭据,并确认 `AGENTBUS_LOG_PAYLOADS=false`。首次部署 `0.5.167` 时保持 `EXTENSION_AUTO_UPDATE_ENABLED=false`;只有完成共享目录引导、HTTPS 可达性、受限 ECS RAM 凭据和账号实例映射后再启用。生产数据库可使用现有 PostgreSQL 实例中的新 Schema;迁移程序会创建 Schema,不会触碰其他 Schema 的测试表。
+1. 复制 `.env.production.example` 为部署机受保护的 `.env.production`,填入 `DEPLOYMENT_REVISION`、PostgreSQL URL、`DATABASE_SCHEMA`、字段加密密钥、外部解析 Key 和 OSS 凭据,并确认 `AGENTBUS_LOG_PAYLOADS=false`。生产数据库可使用现有 PostgreSQL 实例中的新 Schema;迁移程序会创建 Schema,不会触碰其他 Schema 的测试表。
2. 在正式数据库上线前执行并验证备份:`infra/backup-postgres.sh`。
3. 使用 `docker compose --env-file .env.production up -d --build` 启动;Compose 会先执行数据库迁移,再启动控制平面。Compose 中的本地 PostgreSQL 仅用于 `--profile local`,生产 `DATABASE_URL` 指向受保护的远程数据库。
4. 首次启动后在容器内通过 `docker compose exec -e ADMIN_USERNAME=admin -e ADMIN_PASSWORD='replace-with-password' control-plane node .build/control-plane/src/admin-cli.js bootstrap` 创建管理员;不要把密码写入镜像或 Git。
diff --git a/control-plane/migrations/019_extension_host_updates.sql b/control-plane/migrations/019_extension_host_updates.sql
deleted file mode 100644
index 97ffce2..0000000
--- a/control-plane/migrations/019_extension_host_updates.sql
+++ /dev/null
@@ -1,77 +0,0 @@
--- Bind browser accounts to Alibaba ECS Windows hosts and persist signed-in
--- browser extension releases/update orchestration. The updater remains part of
--- the control-plane process; target hosts only run one-shot Cloud Assistant
--- commands after every browser worker on that host reports an idle state.
-
-ALTER TABLE users
- ADD COLUMN IF NOT EXISTS extension_ecs_region_id text,
- ADD COLUMN IF NOT EXISTS extension_ecs_instance_id text;
-
-ALTER TABLE users
- DROP CONSTRAINT IF EXISTS users_extension_ecs_binding_pair_check;
-
-ALTER TABLE users
- ADD CONSTRAINT users_extension_ecs_binding_pair_check
- CHECK (
- (extension_ecs_region_id IS NULL AND extension_ecs_instance_id IS NULL)
- OR (
- extension_ecs_region_id ~ '^[a-z0-9][a-z0-9-]{0,63}$'
- AND extension_ecs_instance_id ~ '^i-[A-Za-z0-9]{6,64}$'
- )
- );
-
-CREATE INDEX IF NOT EXISTS users_extension_ecs_host_idx
- ON users (organization_id, extension_ecs_region_id, extension_ecs_instance_id)
- WHERE extension_ecs_region_id IS NOT NULL AND extension_ecs_instance_id IS NOT NULL;
-
-CREATE TABLE IF NOT EXISTS extension_releases (
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
- organization_id uuid NOT NULL REFERENCES organizations(id),
- version text NOT NULL,
- object_key text NOT NULL,
- sha256 text NOT NULL,
- byte_size integer NOT NULL,
- extension_manifest jsonb NOT NULL,
- is_active boolean NOT NULL DEFAULT false,
- published_by uuid NOT NULL REFERENCES users(id),
- created_at timestamptz NOT NULL DEFAULT now(),
- UNIQUE (organization_id, version),
- UNIQUE (organization_id, object_key),
- CHECK (version ~ '^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$'),
- CHECK (sha256 ~ '^[a-f0-9]{64}$'),
- CHECK (byte_size BETWEEN 1 AND 50000000),
- CHECK (char_length(object_key) BETWEEN 1 AND 500)
-);
-
-CREATE UNIQUE INDEX IF NOT EXISTS extension_releases_one_active_idx
- ON extension_releases (organization_id)
- WHERE is_active = true;
-
-CREATE INDEX IF NOT EXISTS extension_releases_created_idx
- ON extension_releases (organization_id, created_at DESC);
-
-CREATE TABLE IF NOT EXISTS extension_host_updates (
- organization_id uuid NOT NULL REFERENCES organizations(id),
- ecs_region_id text NOT NULL,
- ecs_instance_id text NOT NULL,
- release_id uuid NOT NULL REFERENCES extension_releases(id),
- status text NOT NULL,
- attempt_count integer NOT NULL DEFAULT 0,
- invocation_id text,
- error_code text,
- error_message text,
- started_at timestamptz,
- deployed_at timestamptz,
- verified_at timestamptz,
- updated_at timestamptz NOT NULL DEFAULT now(),
- PRIMARY KEY (organization_id, ecs_region_id, ecs_instance_id),
- CHECK (status IN ('waiting_for_idle', 'running', 'deployed', 'verified', 'failed')),
- CHECK (attempt_count BETWEEN 0 AND 3),
- CHECK (ecs_region_id ~ '^[a-z0-9][a-z0-9-]{0,63}$'),
- CHECK (ecs_instance_id ~ '^i-[A-Za-z0-9]{6,64}$'),
- CHECK (error_code IS NULL OR char_length(error_code) <= 120),
- CHECK (error_message IS NULL OR char_length(error_message) <= 2000)
-);
-
-CREATE INDEX IF NOT EXISTS extension_host_updates_release_status_idx
- ON extension_host_updates (organization_id, release_id, status, updated_at DESC);
diff --git a/control-plane/src/auth.ts b/control-plane/src/auth.ts
index ce5c9eb..7123182 100644
--- a/control-plane/src/auth.ts
+++ b/control-plane/src/auth.ts
@@ -28,8 +28,6 @@ export interface PublicAccount {
username: string;
role: AuthRole;
erp_account: string | null;
- extension_ecs_region_id: string | null;
- extension_ecs_instance_id: string | null;
is_active: boolean;
authorized_business_route_ids: BusinessRouteId[];
business_authorization_revision: number;
@@ -103,42 +101,6 @@ function validateAccountRouting(role: AuthRole, value: unknown): string | null {
return erpAccount;
}
-function normalizeEcsRegionId(value: unknown): string | null {
- const normalized = String(value ?? '').trim().toLocaleLowerCase('en-US');
- if (!normalized) return null;
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(normalized)) {
- throw new AuthError('extension_ecs_region_invalid', 'ECS 地域 ID 格式无效。', 400);
- }
- return normalized;
-}
-
-function normalizeEcsInstanceId(value: unknown): string | null {
- const normalized = String(value ?? '').trim();
- if (!normalized) return null;
- if (!/^i-[A-Za-z0-9]{6,64}$/u.test(normalized)) {
- throw new AuthError('extension_ecs_instance_invalid', 'ECS 实例 ID 格式无效。', 400);
- }
- return normalized;
-}
-
-function validateExtensionHostBinding(
- role: AuthRole,
- regionValue: unknown,
- instanceValue: unknown
-): { regionId: string | null; instanceId: string | null } {
- const regionId = normalizeEcsRegionId(regionValue);
- const instanceId = normalizeEcsInstanceId(instanceValue);
- if (role === 'admin' && (regionId || instanceId)) {
- throw new AuthError('admin_extension_host_forbidden', '管理员账号不能绑定 ERP 插件云主机。', 409);
- }
- if (Boolean(regionId) !== Boolean(instanceId)) {
- throw new AuthError('extension_ecs_binding_incomplete', 'ECS 地域 ID 与实例 ID 必须同时填写或同时清空。', 400);
- }
- return role === 'admin'
- ? { regionId: null, instanceId: null }
- : { regionId, instanceId };
-}
-
function normalizeRole(value: unknown): AuthRole {
if (value === 'admin' || value === 'team_lead') return value;
return 'user';
@@ -185,8 +147,6 @@ function mapAccount(row: Record
): PublicAccount {
username: String(row.username),
role,
erp_account: normalizeErpAccount(row.erp_account),
- extension_ecs_region_id: normalizeEcsRegionId(row.extension_ecs_region_id),
- extension_ecs_instance_id: normalizeEcsInstanceId(row.extension_ecs_instance_id),
is_active: row.is_active === true || String(row.is_active) === 'true',
authorized_business_route_ids: role === 'admin' ? [...ALL_BUSINESS_ROUTE_IDS] : storedRouteIds,
business_authorization_revision: Math.max(0, Number(row.business_authorization_revision || 0)),
@@ -202,8 +162,7 @@ async function loadPublicAccount(
userId: string
): Promise {
const result = await client.query(
- `SELECT u.id, u.username, u.role, u.erp_account,
- u.extension_ecs_region_id, u.extension_ecs_instance_id, u.is_active,
+ `SELECT u.id, u.username, u.role, u.erp_account, u.is_active,
u.business_authorization_revision,
u.last_login_at, u.created_at, u.updated_at,
COALESCE(ARRAY(
@@ -409,8 +368,7 @@ export class AuthService {
async listAccounts(actor: AuthUser): Promise {
this.requireAdmin(actor);
const result = await getPool(this.config).query(
- `SELECT u.id, u.username, u.role, u.erp_account,
- u.extension_ecs_region_id, u.extension_ecs_instance_id, u.is_active,
+ `SELECT u.id, u.username, u.role, u.erp_account, u.is_active,
u.business_authorization_revision,
u.last_login_at, u.created_at, u.updated_at,
COALESCE(ARRAY(
@@ -434,8 +392,6 @@ export class AuthService {
password: string;
role: AuthRole;
erpAccount?: string;
- extensionEcsRegionId?: string;
- extensionEcsInstanceId?: string;
businessRouteIds?: readonly string[];
},
requestId: string
@@ -445,11 +401,6 @@ export class AuthService {
const passwordHash = await argon2.hash(validatePassword(input.password), { type: argon2.argon2id });
const role = normalizeRole(input.role);
const erpAccount = validateAccountRouting(role, input.erpAccount);
- const extensionHost = validateExtensionHostBinding(
- role,
- input.extensionEcsRegionId,
- input.extensionEcsInstanceId
- );
const businessRouteIds = role === 'admin' ? [] : normalizeBusinessRouteIds(input.businessRouteIds);
try {
return await withTransaction(this.config, async (client) => {
@@ -464,19 +415,10 @@ export class AuthService {
if (existing.rowCount) throw new AuthError('account_exists', '该账号已存在。', 409);
const created = await client.query(
`INSERT INTO users
- (organization_id, username, password_hash, role, erp_account,
- extension_ecs_region_id, extension_ecs_instance_id, password_changed_at)
- VALUES ($1, $2, $3, $4, $5, $6, $7, now())
+ (organization_id, username, password_hash, role, erp_account, password_changed_at)
+ VALUES ($1, $2, $3, $4, $5, now())
RETURNING id`,
- [
- actor.organizationId,
- username,
- passwordHash,
- role,
- erpAccount,
- extensionHost.regionId,
- extensionHost.instanceId
- ]
+ [actor.organizationId, username, passwordHash, role, erpAccount]
);
const accountId = String(created.rows[0].id);
if (businessRouteIds.length) {
@@ -493,7 +435,6 @@ export class AuthService {
await this.accountAudit(client, actor, 'account.created', account.id, requestId, {
role,
erp_account_configured: Boolean(erpAccount),
- extension_host_configured: Boolean(extensionHost.instanceId),
authorized_business_route_ids: account.authorized_business_route_ids
});
return account;
@@ -512,30 +453,17 @@ export class AuthService {
async updateAccount(
actor: AuthUser,
targetUserId: string,
- input: {
- role?: AuthRole;
- isActive?: boolean;
- erpAccount?: string | null;
- extensionEcsRegionId?: string | null;
- extensionEcsInstanceId?: string | null;
- },
+ input: { role?: AuthRole; isActive?: boolean; erpAccount?: string | null },
requestId: string
): Promise {
this.requireAdmin(actor);
- if (
- input.role === undefined
- && input.isActive === undefined
- && input.erpAccount === undefined
- && input.extensionEcsRegionId === undefined
- && input.extensionEcsInstanceId === undefined
- ) {
+ if (input.role === undefined && input.isActive === undefined && input.erpAccount === undefined) {
throw new AuthError('account_update_empty', '没有需要更新的账号字段。', 400);
}
try {
return await withTransaction(this.config, async (client) => {
const target = await client.query(
- `SELECT id, username, role, erp_account,
- extension_ecs_region_id, extension_ecs_instance_id, is_active,
+ `SELECT id, username, role, erp_account, is_active,
last_login_at, created_at, updated_at
FROM users
WHERE organization_id = $1 AND id = $2
@@ -552,19 +480,6 @@ export class AuthService {
? null
: input.erpAccount === undefined ? before.erp_account : input.erpAccount
);
- const extensionHost = validateExtensionHostBinding(
- role,
- role === 'admin'
- ? null
- : input.extensionEcsRegionId === undefined
- ? before.extension_ecs_region_id
- : input.extensionEcsRegionId,
- role === 'admin'
- ? null
- : input.extensionEcsInstanceId === undefined
- ? before.extension_ecs_instance_id
- : input.extensionEcsInstanceId
- );
const removesActiveAdmin = before.role === 'admin' && before.is_active && (role !== 'admin' || !isActive);
if (actor.id === before.id && (role !== 'admin' || !isActive)) {
throw new AuthError('self_lockout_forbidden', '不能停用或降级当前登录的管理员账号。', 409);
@@ -582,24 +497,12 @@ export class AuthService {
}
const updated = await client.query(
`UPDATE users
- SET role = $1, is_active = $2, erp_account = $3,
- extension_ecs_region_id = $4, extension_ecs_instance_id = $5,
- updated_at = now()
- WHERE organization_id = $6 AND id = $7
+ SET role = $1, is_active = $2, erp_account = $3, updated_at = now()
+ WHERE organization_id = $4 AND id = $5
RETURNING id`,
- [
- role,
- isActive,
- erpAccount,
- extensionHost.regionId,
- extensionHost.instanceId,
- actor.organizationId,
- before.id
- ]
+ [role, isActive, erpAccount, actor.organizationId, before.id]
);
const routingIdentityChanged = before.erp_account !== erpAccount;
- const extensionHostChanged = before.extension_ecs_region_id !== extensionHost.regionId
- || before.extension_ecs_instance_id !== extensionHost.instanceId;
if (before.role !== role || before.is_active !== isActive || routingIdentityChanged) {
await client.query(
'UPDATE sessions SET revoked_at = now() WHERE user_id = $1 AND revoked_at IS NULL',
@@ -615,7 +518,7 @@ export class AuthService {
[role === 'admin' ? '绑定账号已变更为管理员,渠道已解除绑定。' : '绑定账号已停用,渠道已解除绑定。', actor.organizationId, before.id]
);
}
- if (!isActive || routingIdentityChanged || extensionHostChanged || before.role !== role) {
+ if (!isActive || routingIdentityChanged || before.role !== role) {
await client.query(
`UPDATE browser_connections
SET status = 'superseded', erp_account_verified = false
@@ -629,7 +532,6 @@ export class AuthService {
previous_active: before.is_active,
active: isActive,
erp_account_changed: routingIdentityChanged,
- extension_host_changed: extensionHostChanged,
sessions_revoked: before.role !== role || before.is_active !== isActive || routingIdentityChanged
});
const account = await loadPublicAccount(client, actor.organizationId, String(updated.rows[0].id));
diff --git a/control-plane/src/config.ts b/control-plane/src/config.ts
index df21fb2..b99deb3 100644
--- a/control-plane/src/config.ts
+++ b/control-plane/src/config.ts
@@ -32,20 +32,6 @@ const optionalOssEndpoint = z.preprocess(
z.string().regex(/^[a-z0-9.-]+$/i).optional()
);
-const optionalWindowsExtensionPath = z.preprocess(
- (value) => {
- const normalized = String(value ?? '').trim().replace(/\/+$/u, '');
- return normalized || undefined;
- },
- z.string()
- .max(240)
- .refine(
- (value) => /^[A-Za-z]:\\ProgramData\\LTJT\\[A-Za-z0-9._\\-]+$/u.test(value) && !value.includes('..'),
- 'EXTENSION_WINDOWS_INSTALL_PATH must be an absolute path below :\\ProgramData\\LTJT.'
- )
- .optional()
-);
-
function parseDurationMs(value: string): number {
const normalized = String(value || '').trim().toLowerCase();
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(normalized);
@@ -111,18 +97,6 @@ const envSchema = z.object({
OSS_BUCKET_NAME: optionalString,
OSS_REGION: optionalString,
OSS_KEY_PREFIX: z.string().trim().min(1).max(200).default('liansyn-platform/attachments'),
- EXTENSION_AUTO_UPDATE_ENABLED: z.enum(['true', 'false']).default('false').transform((value) => value === 'true'),
- EXTENSION_UPDATE_OSS_KEY_PREFIX: z.string().trim().min(1).max(200)
- .default('liansyn-platform/chrome-extension')
- .transform((value) => value.replace(/^\/+|\/+$/gu, '')),
- EXTENSION_WINDOWS_INSTALL_PATH: optionalWindowsExtensionPath
- .default('C:\\ProgramData\\LTJT\\chrome-extension\\ltjt-order-assistant'),
- EXTENSION_UPDATE_MAX_PACKAGE_BYTES: z.coerce.number().int().positive().max(50_000_000).default(15_000_000),
- EXTENSION_UPDATE_COMMAND_TIMEOUT_SECONDS: z.coerce.number().int().min(60).max(3_600).default(600),
- EXTENSION_UPDATE_POLL_INTERVAL_MS: z.coerce.number().int().min(1_000).max(60_000).default(5_000),
- ALIBABA_CLOUD_ACCESS_KEY_ID: optionalString,
- ALIBABA_CLOUD_ACCESS_KEY_SECRET: optionalString,
- ALIBABA_CLOUD_SECURITY_TOKEN: optionalString,
DATA_RETENTION_ENABLED: z.enum(['true', 'false']).default('false').transform((value) => value === 'true'),
DATA_RETENTION_DAYS: z.coerce.number().int().positive().default(180),
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'silent']).default('info')
@@ -186,23 +160,6 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
if (parsed.ARTIFACT_STORAGE_BACKEND === 'oss' && !ossRegion) {
throw new Error('OSS_REGION is required when OSS_ENDPOINT does not use the standard oss- endpoint format.');
}
- if (parsed.EXTENSION_AUTO_UPDATE_ENABLED) {
- const missing = [
- ['OSS_ACCESS_KEY_ID', parsed.OSS_ACCESS_KEY_ID],
- ['OSS_ACCESS_KEY_SECRET', parsed.OSS_ACCESS_KEY_SECRET],
- ['OSS_ENDPOINT', parsed.OSS_ENDPOINT],
- ['OSS_BUCKET_NAME', parsed.OSS_BUCKET_NAME],
- ['OSS_REGION', ossRegion],
- ['ALIBABA_CLOUD_ACCESS_KEY_ID', parsed.ALIBABA_CLOUD_ACCESS_KEY_ID],
- ['ALIBABA_CLOUD_ACCESS_KEY_SECRET', parsed.ALIBABA_CLOUD_ACCESS_KEY_SECRET]
- ].filter(([, value]) => !value).map(([name]) => name);
- if (missing.length) {
- throw new Error(`Extension auto-update is enabled but missing configuration: ${missing.join(', ')}`);
- }
- if (parsed.NODE_ENV === 'production' && new URL(parsed.APP_ORIGIN).protocol !== 'https:') {
- throw new Error('APP_ORIGIN must use HTTPS when extension auto-update is enabled in production.');
- }
- }
return {
...parsed,
fieldEncryptionKey: decodeEncryptionKey(parsed.FIELD_ENCRYPTION_KEY, parsed.NODE_ENV),
diff --git a/control-plane/src/db.ts b/control-plane/src/db.ts
index c64a9d3..fe94de2 100644
--- a/control-plane/src/db.ts
+++ b/control-plane/src/db.ts
@@ -5,7 +5,7 @@ import { writeEmergencyDiagnostic } from './diagnostics.js';
const { Pool } = pg;
let pool: pg.Pool | null = null;
-export const REQUIRED_SCHEMA_VERSION = '019_extension_host_updates';
+export const REQUIRED_SCHEMA_VERSION = '018_agentbus_account_workers';
export interface DatabaseReadiness {
ready: boolean;
diff --git a/control-plane/src/diagnostics.ts b/control-plane/src/diagnostics.ts
index f3109ca..34ed5a7 100644
--- a/control-plane/src/diagnostics.ts
+++ b/control-plane/src/diagnostics.ts
@@ -97,17 +97,9 @@ export function diagnosticRequestPath(value: unknown): string {
const raw = text(value);
if (!raw) return '/';
try {
- const pathname = new URL(raw, 'http://diagnostic.invalid').pathname;
- if (pathname.startsWith('/api/extension-updates/package/')) {
- return '/api/extension-updates/package/:token';
- }
- return pathname.slice(0, 500) || '/';
+ return new URL(raw, 'http://diagnostic.invalid').pathname.slice(0, 500) || '/';
} catch {
- const pathname = raw.split(/[?#]/u, 1)[0];
- if (pathname.startsWith('/api/extension-updates/package/')) {
- return '/api/extension-updates/package/:token';
- }
- return pathname.slice(0, 500) || '/';
+ return raw.split(/[?#]/u, 1)[0].slice(0, 500) || '/';
}
}
diff --git a/control-plane/src/extension-updates.ts b/control-plane/src/extension-updates.ts
deleted file mode 100644
index 2e44080..0000000
--- a/control-plane/src/extension-updates.ts
+++ /dev/null
@@ -1,1276 +0,0 @@
-import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
-import EcsPackage, {
- DescribeInvocationResultsRequest,
- RunCommandRequest
-} from '@alicloud/ecs20140526';
-import { $OpenApiUtil } from '@alicloud/openapi-core';
-import { fromBufferPromise, type Entry, type ZipFile } from 'yauzl';
-import type { AppConfig } from './config.js';
-import { getPool, withTransaction } from './db.js';
-import {
- AliyunOssClient,
- type OssObjectClient
-} from './oss-client.js';
-import {
- noopDiagnosticLogger,
- type DiagnosticLogger
-} from './diagnostics.js';
-
-const EXTENSION_NAME = '联泰下单助手';
-const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:\.\d+)?$/u;
-const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
-const TERMINAL_FAILURE_STATUSES = new Set([
- 'invalid',
- 'aborted',
- 'failed',
- 'error',
- 'timeout',
- 'cancelled',
- 'terminated',
- 'partialfailed',
- 'stopped'
-]);
-const TERMINAL_SUCCESS_STATUSES = new Set(['success', 'finished']);
-const UPDATE_COMMAND_RESULT_GRACE_MS = 60_000;
-const MAX_ARCHIVE_ENTRIES = 500;
-const MAX_UNCOMPRESSED_BYTES = 50_000_000;
-
-export type ExtensionUpdateClientStatus =
- | 'disabled'
- | 'no_release'
- | 'unconfigured'
- | 'current'
- | 'waiting_for_idle'
- | 'updating'
- | 'reload_required'
- | 'failed';
-
-export interface ExtensionRelease {
- id: string;
- version: string;
- sha256: string;
- byte_size: number;
- created_at: string;
- is_active: boolean;
-}
-
-interface StoredExtensionRelease extends ExtensionRelease {
- organization_id: string;
- object_key: string;
- extension_manifest: Record;
-}
-
-export interface ExtensionUpdateHeartbeatTarget {
- ecsRegionId: string | null;
- ecsInstanceId: string | null;
- serverUpdateSafe: boolean;
-}
-
-export interface ExtensionUpdateObservation {
- organizationId: string;
- userId: string;
- connectionId: string;
- currentVersion: string;
- extensionUpdateSafe: boolean;
- target: ExtensionUpdateHeartbeatTarget;
-}
-
-export interface ExtensionUpdateClientState {
- status: ExtensionUpdateClientStatus;
- current_version: string;
- target_version: string;
- action?: 'reload';
- message: string;
-}
-
-export interface ExtensionPackageDownload {
- content: Buffer;
- fileName: string;
-}
-
-export interface ExtensionHostCommandInput {
- regionId: string;
- instanceId: string;
- script: string;
- timeoutSeconds: number;
- clientToken: string;
-}
-
-export interface ExtensionHostCommandResult {
- status: 'running' | 'success' | 'failed';
- exitCode?: number;
- output?: string;
- errorCode?: string;
- errorMessage?: string;
-}
-
-export interface ExtensionHostCommandClient {
- runPowerShell(input: ExtensionHostCommandInput): Promise<{ invocationId: string }>;
- getInvocation(input: {
- regionId: string;
- instanceId: string;
- invocationId: string;
- }): Promise;
-}
-
-export function interpretCloudAssistantInvocation(item: Record): ExtensionHostCommandResult {
- const rawStatus = String(item.invocationStatus || item.invokeRecordStatus || '').toLocaleLowerCase('en-US');
- const output = String(item.output || '').slice(0, 2_000);
- const exitCode = Number(item.exitCode);
- const completionMarkerPresent = /(?:^|\r?\n)LTJT_EXTENSION_(?:UPDATED|NEWER_PRESENT) \d+\.\d+\.\d+(?:\.\d+)?(?:\r?\n|$)/u.test(output);
- if (TERMINAL_SUCCESS_STATUSES.has(rawStatus) && exitCode === 0 && completionMarkerPresent) {
- return { status: 'success', exitCode, output };
- }
- if (TERMINAL_FAILURE_STATUSES.has(rawStatus) || TERMINAL_SUCCESS_STATUSES.has(rawStatus)) {
- return {
- status: 'failed',
- exitCode,
- output,
- errorCode: String(
- item.errorCode
- || (exitCode === 0 && !completionMarkerPresent ? 'extension_update_marker_missing' : '')
- || rawStatus
- || 'cloud_assistant_failed'
- ).slice(0, 120),
- errorMessage: String(
- item.errorInfo
- || (exitCode === 0 && !completionMarkerPresent ? '云助手命令缺少插件部署完成标记。' : '')
- || output
- || '云助手命令执行失败。'
- ).slice(0, 2_000)
- };
- }
- return { status: 'running', output };
-}
-
-export interface ExtensionUpdateController {
- observeHeartbeat(input: ExtensionUpdateObservation): Promise;
- publishRelease(input: {
- organizationId: string;
- actorUserId: string;
- requestId: string;
- content: Buffer;
- }): Promise;
- listReleases(organizationId: string): Promise;
- downloadPackage(token: string): Promise;
- close(): void;
-}
-
-export class ExtensionUpdateError extends Error {
- constructor(
- public readonly code: string,
- message: string,
- public readonly statusCode = 400
- ) {
- super(message);
- this.name = 'ExtensionUpdateError';
- }
-}
-
-function normalizeVersion(value: unknown): string {
- const version = String(value ?? '').trim();
- if (!VERSION_PATTERN.test(version)) {
- throw new ExtensionUpdateError('extension_version_invalid', '插件版本号必须是 3—4 段数字。');
- }
- const parts = version.split('.').map(Number);
- if (parts.some((part) => !Number.isSafeInteger(part) || part < 0 || part > 65_535)) {
- throw new ExtensionUpdateError('extension_version_invalid', '插件版本号每一段必须介于 0—65535。');
- }
- return parts.join('.');
-}
-
-export function compareExtensionVersions(left: string, right: string): number {
- const leftParts = normalizeVersion(left).split('.').map(Number);
- const rightParts = normalizeVersion(right).split('.').map(Number);
- const length = Math.max(leftParts.length, rightParts.length);
- for (let index = 0; index < length; index += 1) {
- const difference = (leftParts[index] || 0) - (rightParts[index] || 0);
- if (difference !== 0) return difference > 0 ? 1 : -1;
- }
- return 0;
-}
-
-function iso(value: unknown): string {
- const date = new Date(String(value || ''));
- return Number.isFinite(date.getTime()) ? date.toISOString() : new Date(0).toISOString();
-}
-
-function asRecord(value: unknown): Record {
- return value && typeof value === 'object' && !Array.isArray(value)
- ? value as Record
- : {};
-}
-
-function mapRelease(row: Record): StoredExtensionRelease {
- return {
- id: String(row.id),
- organization_id: String(row.organization_id),
- version: normalizeVersion(row.version),
- object_key: String(row.object_key),
- sha256: String(row.sha256).toLowerCase(),
- byte_size: Number(row.byte_size),
- extension_manifest: asRecord(row.extension_manifest),
- is_active: row.is_active === true || String(row.is_active) === 'true',
- created_at: iso(row.created_at)
- };
-}
-
-function publicRelease(release: StoredExtensionRelease): ExtensionRelease {
- return {
- id: release.id,
- version: release.version,
- sha256: release.sha256,
- byte_size: release.byte_size,
- is_active: release.is_active,
- created_at: release.created_at
- };
-}
-
-function safeArchivePath(fileName: string): string {
- const normalized = String(fileName || '').replace(/\\/gu, '/');
- const parts = normalized.split('/');
- if (
- !normalized
- || normalized.length > 500
- || normalized.startsWith('/')
- || normalized.includes('\u0000')
- || parts.some((part) => (
- !/^[A-Za-z0-9][A-Za-z0-9._-]{0,119}$/u.test(part)
- || part === '.'
- || part === '..'
- || part.endsWith('.')
- || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(part)
- ))
- ) {
- throw new ExtensionUpdateError('extension_package_path_invalid', '插件包包含不安全的文件路径。');
- }
- return normalized;
-}
-
-async function readArchiveEntry(zipFile: ZipFile, entry: Entry, maxBytes: number): Promise {
- if (entry.uncompressedSize > maxBytes) {
- throw new ExtensionUpdateError('extension_package_manifest_too_large', '插件 manifest.json 过大。');
- }
- const stream = await zipFile.openReadStreamPromise(entry);
- const chunks: Buffer[] = [];
- let total = 0;
- for await (const chunk of stream) {
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
- total += buffer.byteLength;
- if (total > maxBytes) {
- throw new ExtensionUpdateError('extension_package_manifest_too_large', '插件 manifest.json 过大。');
- }
- chunks.push(buffer);
- }
- return Buffer.concat(chunks);
-}
-
-export async function inspectExtensionPackage(content: Buffer): Promise<{
- version: string;
- sha256: string;
- manifest: Record;
- entryCount: number;
-}> {
- let zipFile: ZipFile;
- try {
- zipFile = await fromBufferPromise(content, {
- lazyEntries: true,
- decodeStrings: true,
- validateEntrySizes: true,
- strictFileNames: true
- });
- } catch {
- throw new ExtensionUpdateError('extension_package_invalid_zip', '上传文件不是有效的插件 ZIP。');
- }
-
- const paths = new Set();
- let manifestSource: Buffer | null = null;
- let entryCount = 0;
- let uncompressedBytes = 0;
- try {
- for await (const entry of zipFile.eachEntry()) {
- entryCount += 1;
- uncompressedBytes += entry.uncompressedSize;
- if (
- entryCount > MAX_ARCHIVE_ENTRIES
- || entry.uncompressedSize > MAX_UNCOMPRESSED_BYTES
- || uncompressedBytes > MAX_UNCOMPRESSED_BYTES
- ) {
- throw new ExtensionUpdateError('extension_package_archive_limit', '插件包展开后超过安全上限。');
- }
- if (entry.isEncrypted() || !entry.canDecodeFileData()) {
- throw new ExtensionUpdateError('extension_package_encrypted', '插件包不能包含加密文件。');
- }
- const path = safeArchivePath(entry.fileName);
- const lowerPath = path.toLocaleLowerCase('en-US');
- if (paths.has(lowerPath)) {
- throw new ExtensionUpdateError('extension_package_duplicate_path', '插件包包含重复文件路径。');
- }
- paths.add(lowerPath);
- const unixMode = (entry.externalFileAttributes >>> 16) & 0o170000;
- if (unixMode === 0o120000) {
- throw new ExtensionUpdateError('extension_package_symlink_forbidden', '插件包不能包含符号链接。');
- }
- if (path === 'manifest.json') manifestSource = await readArchiveEntry(zipFile, entry, 65_536);
- }
- } catch (error) {
- if (error instanceof ExtensionUpdateError) throw error;
- throw new ExtensionUpdateError('extension_package_invalid_zip', '插件 ZIP 内容无法安全读取。');
- } finally {
- zipFile.close();
- }
-
- if (!manifestSource || !paths.has('background.js') || !paths.has('business-bridge.js')) {
- throw new ExtensionUpdateError(
- 'extension_package_files_missing',
- '插件包根目录必须包含 manifest.json、background.js 和 business-bridge.js。'
- );
- }
- let manifest: Record;
- try {
- manifest = asRecord(JSON.parse(manifestSource.toString('utf8')));
- } catch {
- throw new ExtensionUpdateError('extension_manifest_invalid', '插件 manifest.json 不是有效 JSON。');
- }
- if (manifest.manifest_version !== 3 || String(manifest.name || '') !== EXTENSION_NAME) {
- throw new ExtensionUpdateError('extension_manifest_identity_mismatch', '插件包身份与联泰下单助手不匹配。');
- }
- const background = asRecord(manifest.background);
- const contentScripts = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
- const bridgeDeclared = contentScripts.some((entry) => {
- const script = asRecord(entry);
- return Array.isArray(script.js) && script.js.map(String).includes('business-bridge.js');
- });
- if (String(background.service_worker || '') !== 'background.js' || !bridgeDeclared) {
- throw new ExtensionUpdateError(
- 'extension_manifest_contract_mismatch',
- '插件 Manifest 未声明预期的后台脚本与平台桥接脚本。'
- );
- }
- const version = normalizeVersion(manifest.version);
- return {
- version,
- manifest,
- entryCount,
- sha256: createHash('sha256').update(content).digest('hex')
- };
-}
-
-interface DownloadTokenPayload {
- releaseId: string;
- organizationId: string;
- regionId: string;
- instanceId: string;
- expiresAt: number;
-}
-
-export function createExtensionDownloadToken(payload: DownloadTokenPayload, key: Buffer): string {
- const encoded = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url');
- const signature = createHmac('sha256', key)
- .update('ltjt-extension-download-v1\0')
- .update(encoded)
- .digest('base64url');
- return `${encoded}.${signature}`;
-}
-
-export function verifyExtensionDownloadToken(token: string, key: Buffer, now = Date.now()): DownloadTokenPayload {
- if (!token || token.length > 4_096) {
- throw new ExtensionUpdateError('extension_download_token_invalid', '插件下载令牌无效。', 401);
- }
- const [encoded, signature, extra] = String(token || '').split('.');
- if (!encoded || !signature || extra) {
- throw new ExtensionUpdateError('extension_download_token_invalid', '插件下载令牌无效。', 401);
- }
- const expected = createHmac('sha256', key)
- .update('ltjt-extension-download-v1\0')
- .update(encoded)
- .digest();
- let actual: Buffer;
- try {
- actual = Buffer.from(signature, 'base64url');
- } catch {
- throw new ExtensionUpdateError('extension_download_token_invalid', '插件下载令牌无效。', 401);
- }
- if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
- throw new ExtensionUpdateError('extension_download_token_invalid', '插件下载令牌无效。', 401);
- }
- let payload: DownloadTokenPayload;
- try {
- payload = JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8')) as DownloadTokenPayload;
- } catch {
- throw new ExtensionUpdateError('extension_download_token_invalid', '插件下载令牌无效。', 401);
- }
- if (
- !payload.releaseId
- || !payload.organizationId
- || !payload.regionId
- || !payload.instanceId
- || !Number.isSafeInteger(payload.expiresAt)
- ) {
- throw new ExtensionUpdateError('extension_download_token_invalid', '插件下载令牌无效。', 401);
- }
- if (payload.expiresAt <= now) {
- throw new ExtensionUpdateError('extension_download_token_expired', '插件下载令牌已过期。', 401);
- }
- return payload;
-}
-
-export function buildExtensionUpdatePowerShell(input: {
- downloadUrl: string;
- sha256: string;
- version: string;
- installPath: string;
- releaseId: string;
-}): string {
- if (!SHA256_PATTERN.test(input.sha256)) throw new Error('Invalid extension SHA-256.');
- const encodedConfiguration = Buffer.from(JSON.stringify({
- downloadUrl: input.downloadUrl,
- sha256: input.sha256,
- version: normalizeVersion(input.version),
- installPath: input.installPath,
- releaseId: input.releaseId,
- extensionName: EXTENSION_NAME
- }), 'utf8').toString('base64');
- return String.raw`$ErrorActionPreference = 'Stop'
-$ProgressPreference = 'SilentlyContinue'
-[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
-$configJson = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedConfiguration}'))
-$config = $configJson | ConvertFrom-Json
-$updateRoot = Join-Path $env:ProgramData ('LTJT\ExtensionUpdates\' + $config.releaseId)
-$packagePath = Join-Path $updateRoot 'extension.zip'
-$stagingPath = Join-Path $updateRoot 'staging'
-$installPath = [IO.Path]::GetFullPath([string]$config.installPath)
-$allowedRoot = [IO.Path]::GetFullPath((Join-Path $env:ProgramData 'LTJT'))
-$allowedPrefix = $allowedRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
-if (-not $installPath.StartsWith($allowedPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'InstallPathOutsideAllowedRoot' }
-$installedManifestPath = Join-Path $installPath 'manifest.json'
-if (Test-Path -LiteralPath $installedManifestPath -PathType Leaf) {
- try {
- $installedManifest = Get-Content -LiteralPath $installedManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
- $installedVersion = [Version]::Parse([string]$installedManifest.version)
- $targetVersion = [Version]::Parse([string]$config.version)
- if ($installedVersion -gt $targetVersion) {
- Write-Output ('LTJT_EXTENSION_NEWER_PRESENT ' + [string]$installedManifest.version)
- exit 0
- }
- } catch {
- # Invalid existing files are replaced only after the downloaded package
- # passes the server-bound hash and manifest checks below.
- }
-}
-if (Test-Path -LiteralPath $updateRoot) { Remove-Item -LiteralPath $updateRoot -Recurse -Force }
-New-Item -ItemType Directory -Path $stagingPath -Force | Out-Null
-Invoke-WebRequest -Uri ([string]$config.downloadUrl) -OutFile $packagePath -UseBasicParsing
-$actualHash = (Get-FileHash -LiteralPath $packagePath -Algorithm SHA256).Hash.ToLowerInvariant()
-if ($actualHash -ne ([string]$config.sha256).ToLowerInvariant()) { throw 'PackageHashMismatch' }
-Expand-Archive -LiteralPath $packagePath -DestinationPath $stagingPath -Force
-$manifestPath = Join-Path $stagingPath 'manifest.json'
-if (-not (Test-Path -LiteralPath $manifestPath -PathType Leaf)) { throw 'ManifestMissing' }
-$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
-if ([string]$manifest.name -ne [string]$config.extensionName) { throw 'ExtensionIdentityMismatch' }
-if ([string]$manifest.version -ne [string]$config.version) { throw 'ExtensionVersionMismatch' }
-$backupPath = $installPath + '.previous'
-$hadPrevious = Test-Path -LiteralPath $installPath
-if (Test-Path -LiteralPath $backupPath) { Remove-Item -LiteralPath $backupPath -Recurse -Force }
-if ($hadPrevious) { Move-Item -LiteralPath $installPath -Destination $backupPath -Force }
-try {
- Move-Item -LiteralPath $stagingPath -Destination $installPath -Force
-} catch {
- if ($hadPrevious -and (Test-Path -LiteralPath $backupPath) -and -not (Test-Path -LiteralPath $installPath)) {
- Move-Item -LiteralPath $backupPath -Destination $installPath -Force
- }
- throw
-}
-$statusPath = Join-Path $allowedRoot 'extension-update-status.json'
-@{
- ok = $true
- version = [string]$config.version
- sha256 = [string]$config.sha256
- release_id = [string]$config.releaseId
- installed_at = [DateTime]::UtcNow.ToString('o')
-} | ConvertTo-Json -Compress | Set-Content -LiteralPath $statusPath -Encoding UTF8
-Remove-Item -LiteralPath $updateRoot -Recurse -Force -ErrorAction SilentlyContinue
-Write-Output ('LTJT_EXTENSION_UPDATED ' + [string]$config.version)`;
-}
-
-export class AliyunEcsCloudAssistantClient implements ExtensionHostCommandClient {
- private readonly clients = new Map>();
-
- constructor(private readonly config: AppConfig) {}
-
- private client(regionId: string): InstanceType<(typeof EcsPackage)['default']> {
- let client = this.clients.get(regionId);
- if (client) return client;
- const sdkConfig = new $OpenApiUtil.Config({
- accessKeyId: this.config.ALIBABA_CLOUD_ACCESS_KEY_ID,
- accessKeySecret: this.config.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
- securityToken: this.config.ALIBABA_CLOUD_SECURITY_TOKEN,
- regionId,
- endpoint: `ecs.${regionId}.aliyuncs.com`,
- connectTimeout: 10_000,
- readTimeout: 20_000
- });
- client = new EcsPackage.default(sdkConfig);
- this.clients.set(regionId, client);
- return client;
- }
-
- async runPowerShell(input: ExtensionHostCommandInput): Promise<{ invocationId: string }> {
- const commandContent = Buffer.from(input.script, 'utf8').toString('base64');
- if (Buffer.byteLength(commandContent, 'utf8') > 24 * 1024) {
- throw new Error('Cloud Assistant command exceeds the 24 KB encoded limit.');
- }
- const response = await this.client(input.regionId).runCommand(new RunCommandRequest({
- regionId: input.regionId,
- instanceId: [input.instanceId],
- type: 'RunPowerShellScript',
- commandContent,
- contentEncoding: 'Base64',
- repeatMode: 'Once',
- keepCommand: false,
- terminationMode: 'ProcessTree',
- timeout: input.timeoutSeconds,
- clientToken: input.clientToken,
- name: 'LTJT Chrome extension update',
- description: 'Controlled LTJT extension package replacement after browser idle gate.'
- }));
- const invocationId = String(response.body?.invokeId || '');
- if (!invocationId) throw new Error('Alibaba ECS RunCommand did not return an invocation ID.');
- return { invocationId };
- }
-
- async getInvocation(input: {
- regionId: string;
- instanceId: string;
- invocationId: string;
- }): Promise {
- const response = await this.client(input.regionId).describeInvocationResults(
- new DescribeInvocationResultsRequest({
- regionId: input.regionId,
- instanceId: input.instanceId,
- invokeId: input.invocationId,
- contentEncoding: 'PlainText',
- maxResults: 10
- })
- );
- const item = response.body?.invocation?.invocationResults?.invocationResult?.[0];
- if (!item) return { status: 'running' };
- return interpretCloudAssistantInvocation(item as unknown as Record);
- }
-}
-
-function delay(milliseconds: number): Promise {
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
-}
-
-function hostKey(input: { organizationId: string; regionId: string; instanceId: string }): string {
- return `${input.organizationId}:${input.regionId}:${input.instanceId}`;
-}
-
-function sanitizedFailure(error: unknown): { code: string; message: string } {
- const candidate = error && typeof error === 'object' ? error as Record : {};
- const code = String(candidate.code || candidate.name || 'extension_update_failed')
- .replace(/[^A-Za-z0-9._-]/gu, '_')
- .slice(0, 120) || 'extension_update_failed';
- const message = String(candidate.message || error || '插件更新失败。')
- .replace(/https?:\/\/\S+/giu, '[URL_REDACTED]')
- .replace(/[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{32,}/gu, '[TOKEN_REDACTED]')
- .slice(0, 2_000);
- return { code, message };
-}
-
-function definiteUpdateFailure(error: T): T {
- return Object.assign(error, { extensionUpdateFailureDefinite: true });
-}
-
-function cloudApiDefinitelyRejected(error: unknown): boolean {
- const candidate = error && typeof error === 'object' ? error as Record : {};
- const response = asRecord(candidate.response);
- const statusCode = Number(candidate.statusCode || response.statusCode);
- const code = String(candidate.code || '').toLocaleLowerCase('en-US');
- if (/(?:idempotent|request.?timeout)/u.test(code) || statusCode === 408 || statusCode === 409) return false;
- if (Number.isSafeInteger(statusCode) && statusCode >= 400 && statusCode < 500) return true;
- return /(?:invalid|forbidden|unauthorized|notfound|not_found|unsupported|permission)/u.test(code);
-}
-
-export class ExtensionUpdateService implements ExtensionUpdateController {
- private readonly inFlight = new Map>();
- private closed = false;
-
- constructor(
- private readonly config: AppConfig,
- private readonly objectClient: OssObjectClient,
- private readonly commandClient: ExtensionHostCommandClient,
- private readonly logger: DiagnosticLogger = noopDiagnosticLogger
- ) {}
-
- close(): void {
- this.closed = true;
- }
-
- async listReleases(organizationId: string): Promise {
- const result = await getPool(this.config).query(
- `SELECT id, organization_id, version, object_key, sha256, byte_size,
- extension_manifest, is_active, created_at
- FROM extension_releases
- WHERE organization_id = $1
- ORDER BY created_at DESC
- LIMIT 50`,
- [organizationId]
- );
- return (result.rows as Record[]).map(mapRelease).map(publicRelease);
- }
-
- async publishRelease(input: {
- organizationId: string;
- actorUserId: string;
- requestId: string;
- content: Buffer;
- }): Promise {
- this.requireEnabled();
- if (!input.content.byteLength || input.content.byteLength > this.config.EXTENSION_UPDATE_MAX_PACKAGE_BYTES) {
- throw new ExtensionUpdateError(
- 'extension_package_size_invalid',
- `插件包必须小于 ${this.config.EXTENSION_UPDATE_MAX_PACKAGE_BYTES} 字节。`,
- 413
- );
- }
- const inspected = await inspectExtensionPackage(input.content);
- const objectKey = `${this.config.EXTENSION_UPDATE_OSS_KEY_PREFIX}/${inspected.version}/${inspected.sha256}/ltjt-order-assistant-${inspected.version}.zip`;
- const existing = await getPool(this.config).query(
- `SELECT id, organization_id, version, object_key, sha256, byte_size,
- extension_manifest, is_active, created_at
- FROM extension_releases
- WHERE organization_id = $1 AND version = $2`,
- [input.organizationId, inspected.version]
- );
- if (existing.rowCount) {
- const release = mapRelease(existing.rows[0] as Record);
- if (release.sha256 !== inspected.sha256) {
- throw new ExtensionUpdateError(
- 'extension_release_version_conflict',
- '该版本已经发布且文件哈希不同;请递增插件版本。',
- 409
- );
- }
- return publicRelease(release);
- }
- const active = await this.activeRelease(input.organizationId);
- if (active && compareExtensionVersions(inspected.version, active.version) <= 0) {
- throw new ExtensionUpdateError(
- 'extension_release_not_newer',
- `新版本必须高于当前活动版本 ${active.version}。`,
- 409
- );
- }
-
- await this.objectClient.putObject(objectKey, {
- content: input.content,
- contentType: 'application/zip',
- contentDisposition: `attachment; filename="ltjt-order-assistant-${inspected.version}.zip"`,
- acl: 'private'
- });
- try {
- const release = await withTransaction(this.config, async (client) => {
- await client.query(
- `SELECT pg_advisory_xact_lock(hashtextextended($1::text || ':extension-release', 0))`,
- [input.organizationId]
- );
- await client.query('SELECT id FROM organizations WHERE id = $1 FOR UPDATE', [input.organizationId]);
- const current = await client.query(
- `SELECT version FROM extension_releases
- WHERE organization_id = $1 AND is_active = true
- FOR UPDATE`,
- [input.organizationId]
- );
- if (current.rowCount && compareExtensionVersions(inspected.version, String(current.rows[0].version)) <= 0) {
- throw new ExtensionUpdateError(
- 'extension_release_not_newer',
- `新版本必须高于当前活动版本 ${String(current.rows[0].version)}。`,
- 409
- );
- }
- await client.query(
- 'UPDATE extension_releases SET is_active = false WHERE organization_id = $1 AND is_active = true',
- [input.organizationId]
- );
- const inserted = await client.query(
- `INSERT INTO extension_releases
- (organization_id, version, object_key, sha256, byte_size, extension_manifest, is_active, published_by)
- VALUES ($1, $2, $3, $4, $5, $6, true, $7)
- RETURNING id, organization_id, version, object_key, sha256, byte_size,
- extension_manifest, is_active, created_at`,
- [
- input.organizationId,
- inspected.version,
- objectKey,
- inspected.sha256,
- input.content.byteLength,
- inspected.manifest,
- input.actorUserId
- ]
- );
- const mapped = mapRelease(inserted.rows[0] as Record);
- await client.query(
- `INSERT INTO audit_events
- (organization_id, actor_user_id, event_type, entity_type, entity_id, request_id, metadata)
- VALUES ($1, $2, 'extension.release.published', 'extension_release', $3, $4, $5)`,
- [input.organizationId, input.actorUserId, mapped.id, input.requestId, {
- version: mapped.version,
- sha256: mapped.sha256,
- byte_size: mapped.byte_size,
- archive_entries: inspected.entryCount
- }]
- );
- return mapped;
- });
- this.logger.info({
- diagnostic_event: 'extension.release.published',
- diagnostic_stage: 'extension_update',
- version: release.version,
- byte_size: release.byte_size
- }, 'extension release published');
- return publicRelease(release);
- } catch (error) {
- const persisted = await getPool(this.config).query(
- `SELECT 1 FROM extension_releases
- WHERE organization_id = $1 AND version = $2 AND sha256 = $3`,
- [input.organizationId, inspected.version, inspected.sha256]
- ).catch(() => ({ rowCount: 0 }));
- if (!persisted.rowCount) await this.objectClient.deleteObject(objectKey).catch(() => {});
- throw error;
- }
- }
-
- async observeHeartbeat(input: ExtensionUpdateObservation): Promise {
- const currentVersion = String(input.currentVersion || '').trim();
- if (!this.config.EXTENSION_AUTO_UPDATE_ENABLED) {
- const regionId = input.target.ecsRegionId;
- const instanceId = input.target.ecsInstanceId;
- const row = regionId && instanceId
- ? await this.hostUpdate(input.organizationId, regionId, instanceId)
- : null;
- const pendingRelease = row
- ? await this.releaseById(input.organizationId, String(row.release_id || ''))
- : null;
- if (row && pendingRelease && String(row.status) !== 'running') {
- try {
- if (compareExtensionVersions(currentVersion, pendingRelease.version) >= 0) {
- await this.markVerified(input, pendingRelease);
- return this.state('disabled', currentVersion, '', '插件自动更新未启用;当前浏览器已完成遗留版本验版。');
- }
- } catch {
- // The normal disabled response below remains fail-closed against the
- // static minimum extension version enforced by the platform.
- }
- }
- if (row && pendingRelease && String(row.status) === 'deployed') {
- return {
- ...this.state('reload_required', currentVersion, pendingRelease.version, '自动更新已停用,但已部署的插件文件仍需安全重载完成收尾。'),
- action: 'reload'
- };
- }
- if (row && pendingRelease && String(row.status) === 'running') {
- const invocationId = String(row.invocation_id || '');
- if (
- invocationId
- && this.config.ALIBABA_CLOUD_ACCESS_KEY_ID
- && this.config.ALIBABA_CLOUD_ACCESS_KEY_SECRET
- ) {
- this.ensureUpdateRun(input, pendingRelease, invocationId);
- return this.state('updating', currentVersion, pendingRelease.version, '自动更新已停用;正在只读收敛已经启动的云助手命令。');
- }
- return this.state('failed', currentVersion, pendingRelease.version, '自动更新已停用,但云主机仍有未收敛的更新命令;请恢复原配置完成收尾。');
- }
- if (row && String(row.status) === 'waiting_for_idle') {
- return this.state('failed', currentVersion, pendingRelease?.version || '', '自动更新已停用,但云主机仍有等待中的更新;请恢复原配置或人工处置。');
- }
- return this.state('disabled', currentVersion, '', '插件自动更新未启用。');
- }
- const release = await this.activeRelease(input.organizationId);
- if (!release) return this.state('no_release', currentVersion, '', '服务端尚未发布插件版本。');
- const regionId = input.target.ecsRegionId;
- const instanceId = input.target.ecsInstanceId;
- const row = regionId && instanceId
- ? await this.hostUpdate(input.organizationId, regionId, instanceId)
- : null;
- if (row && String(row.status) === 'running') {
- const runningRelease = String(row.release_id) === release.id
- ? release
- : await this.releaseById(input.organizationId, String(row.release_id));
- if (!runningRelease) {
- return this.state('failed', currentVersion, release.version, '云主机存在无法识别的旧版本部署记录,已停止自动更新。');
- }
- this.ensureUpdateRun(input, runningRelease, String(row.invocation_id || '') || undefined);
- return this.state(
- 'updating',
- currentVersion,
- release.version,
- runningRelease.id === release.id
- ? '云助手正在部署插件更新。'
- : '云主机正在安全完成已启动的上一版本部署,随后会直接更新到最新版本。'
- );
- }
-
- try {
- if (compareExtensionVersions(currentVersion, release.version) >= 0) {
- if (regionId && instanceId) {
- await this.markVerified(input, release);
- }
- return this.state('current', currentVersion, release.version, '插件已是最新版本。');
- }
- } catch {
- return this.state('failed', currentVersion, release.version, '插件上报的版本号无效,已停止自动更新。');
- }
-
- if (!regionId || !instanceId) {
- return this.state('unconfigured', currentVersion, release.version, '当前平台账号尚未绑定 ECS 云主机。');
- }
- if (row && String(row.release_id) === release.id) {
- const status = String(row.status || '');
- if (status === 'deployed' || status === 'verified') {
- return {
- ...this.state('reload_required', currentVersion, release.version, '新插件文件已部署,等待当前浏览器安全重载。'),
- action: 'reload'
- };
- }
- if (status === 'failed') {
- const attempts = Math.max(0, Number(row.attempt_count || 0));
- if (attempts < 3 && input.extensionUpdateSafe && input.target.serverUpdateSafe) {
- const restarted = await this.markRunning(input, release);
- if (restarted) {
- this.ensureUpdateRun(input, release);
- return this.state('updating', currentVersion, release.version, `正在自动重试插件部署(第 ${attempts + 1}/3 次)。`);
- }
- }
- if (attempts < 3) {
- await this.markWaiting(input, release);
- return this.state('waiting_for_idle', currentVersion, release.version, '上次部署未成功;本机空闲后会自动重试。');
- }
- return this.state(
- 'failed',
- currentVersion,
- release.version,
- `插件更新失败:${String(row.error_message || row.error_code || '未知错误').slice(0, 500)}`
- );
- }
- }
-
- if (!input.extensionUpdateSafe || !input.target.serverUpdateSafe) {
- await this.markWaiting(input, release);
- return this.state('waiting_for_idle', currentVersion, release.version, '检测到本机仍有 ERP 执行或待回查记录,更新将在完全空闲后开始。');
- }
- const started = await this.markRunning(input, release);
- if (!started) {
- return this.state('waiting_for_idle', currentVersion, release.version, '检测到本机仍有 ERP 执行或待回查记录,更新将在完全空闲后开始。');
- }
- this.ensureUpdateRun(input, release);
- return this.state('updating', currentVersion, release.version, '已通过空闲门禁,正在启动云助手更新。');
- }
-
- async downloadPackage(token: string): Promise {
- this.requireEnabled();
- const payload = verifyExtensionDownloadToken(token, this.config.fieldEncryptionKey);
- const result = await getPool(this.config).query(
- `SELECT id, organization_id, version, object_key, sha256, byte_size,
- extension_manifest, is_active, created_at
- FROM extension_releases
- WHERE id = $1 AND organization_id = $2`,
- [payload.releaseId, payload.organizationId]
- );
- if (!result.rowCount) {
- throw new ExtensionUpdateError('extension_release_not_found', '插件发布版本不存在。', 404);
- }
- const release = mapRelease(result.rows[0] as Record);
- if (!this.objectClient.getObject) {
- throw new ExtensionUpdateError('extension_package_store_unreadable', '插件包存储不支持读取。', 503);
- }
- const content = await this.objectClient.getObject(release.object_key);
- if (
- content.byteLength !== release.byte_size
- || createHash('sha256').update(content).digest('hex') !== release.sha256
- ) {
- throw new ExtensionUpdateError('extension_package_integrity_failed', 'OSS 插件包完整性校验失败。', 503);
- }
- return {
- content,
- fileName: `ltjt-order-assistant-${release.version}.zip`
- };
- }
-
- private requireEnabled(): void {
- if (!this.config.EXTENSION_AUTO_UPDATE_ENABLED) {
- throw new ExtensionUpdateError('extension_auto_update_disabled', '插件自动更新未启用。', 409);
- }
- }
-
- private state(
- status: ExtensionUpdateClientStatus,
- currentVersion: string,
- targetVersion: string,
- message: string
- ): ExtensionUpdateClientState {
- return {
- status,
- current_version: currentVersion,
- target_version: targetVersion,
- message
- };
- }
-
- private async activeRelease(organizationId: string): Promise {
- const result = await getPool(this.config).query(
- `SELECT id, organization_id, version, object_key, sha256, byte_size,
- extension_manifest, is_active, created_at
- FROM extension_releases
- WHERE organization_id = $1 AND is_active = true`,
- [organizationId]
- );
- return result.rowCount ? mapRelease(result.rows[0] as Record) : null;
- }
-
- private async releaseById(organizationId: string, releaseId: string): Promise {
- const result = await getPool(this.config).query(
- `SELECT id, organization_id, version, object_key, sha256, byte_size,
- extension_manifest, is_active, created_at
- FROM extension_releases
- WHERE organization_id = $1 AND id = $2`,
- [organizationId, releaseId]
- );
- return result.rowCount ? mapRelease(result.rows[0] as Record) : null;
- }
-
- private async hostUpdate(organizationId: string, regionId: string, instanceId: string): Promise | null> {
- const result = await getPool(this.config).query(
- `SELECT release_id, status, attempt_count, invocation_id, error_code, error_message, updated_at
- FROM extension_host_updates
- WHERE organization_id = $1 AND ecs_region_id = $2 AND ecs_instance_id = $3`,
- [organizationId, regionId, instanceId]
- );
- return result.rowCount ? result.rows[0] as Record : null;
- }
-
- private async markWaiting(input: ExtensionUpdateObservation, release: StoredExtensionRelease): Promise {
- await getPool(this.config).query(
- `INSERT INTO extension_host_updates
- (organization_id, ecs_region_id, ecs_instance_id, release_id, status, attempt_count)
- VALUES ($1, $2, $3, $4, 'waiting_for_idle', 0)
- ON CONFLICT (organization_id, ecs_region_id, ecs_instance_id)
- DO UPDATE SET release_id = EXCLUDED.release_id,
- status = CASE
- WHEN extension_host_updates.release_id = EXCLUDED.release_id
- AND extension_host_updates.status IN ('running', 'deployed', 'verified')
- THEN extension_host_updates.status
- ELSE 'waiting_for_idle'
- END,
- attempt_count = CASE
- WHEN extension_host_updates.release_id = EXCLUDED.release_id
- THEN extension_host_updates.attempt_count
- ELSE 0
- END,
- error_code = NULL, error_message = NULL, updated_at = now()`,
- [input.organizationId, input.target.ecsRegionId, input.target.ecsInstanceId, release.id]
- );
- }
-
- private async markRunning(input: ExtensionUpdateObservation, release: StoredExtensionRelease): Promise {
- return withTransaction(this.config, async (client) => {
- await client.query('SELECT id FROM organizations WHERE id = $1 FOR UPDATE', [input.organizationId]);
- const activeRelease = await client.query(
- `SELECT 1 FROM extension_releases
- WHERE organization_id = $1 AND id = $2 AND is_active = true`,
- [input.organizationId, release.id]
- );
- if (!activeRelease.rowCount) return false;
- const unsafe = await client.query(
- `SELECT (
- EXISTS (
- SELECT 1
- FROM users host_account
- JOIN tasks task
- ON task.organization_id = host_account.organization_id
- AND task.assigned_user_id = host_account.id
- WHERE host_account.organization_id = $1
- AND host_account.extension_ecs_region_id = $2
- AND host_account.extension_ecs_instance_id = $3
- AND (
- task.status = 'reconciliation_pending'
- OR (task.lease_expires_at IS NOT NULL AND task.lease_expires_at > now())
- OR EXISTS (
- SELECT 1 FROM task_attempts attempt
- WHERE attempt.task_id = task.id
- AND attempt.phase = 'erp'
- AND attempt.status IN ('accepted', 'running', 'reconciliation_pending')
- )
- )
- )
- OR EXISTS (
- SELECT 1
- FROM users host_account
- JOIN browser_connections connection
- ON connection.organization_id = host_account.organization_id
- AND connection.user_id = host_account.id
- WHERE host_account.organization_id = $1
- AND host_account.extension_ecs_region_id = $2
- AND host_account.extension_ecs_instance_id = $3
- AND connection.status = 'connected'
- AND connection.last_seen_at >= now() - interval '90 seconds'
- AND lower(COALESCE(connection.metadata->>'extension_update_safe', 'false')) <> 'true'
- )
- ) AS unsafe`,
- [input.organizationId, input.target.ecsRegionId, input.target.ecsInstanceId]
- );
- if (unsafe.rows[0]?.unsafe === true || String(unsafe.rows[0]?.unsafe) === 'true') {
- await client.query(
- `INSERT INTO extension_host_updates
- (organization_id, ecs_region_id, ecs_instance_id, release_id, status, attempt_count)
- VALUES ($1, $2, $3, $4, 'waiting_for_idle', 0)
- ON CONFLICT (organization_id, ecs_region_id, ecs_instance_id)
- DO UPDATE SET release_id = EXCLUDED.release_id, status = 'waiting_for_idle',
- attempt_count = CASE
- WHEN extension_host_updates.release_id = EXCLUDED.release_id
- THEN extension_host_updates.attempt_count
- ELSE 0
- END,
- invocation_id = NULL, error_code = NULL, error_message = NULL,
- updated_at = now()`,
- [input.organizationId, input.target.ecsRegionId, input.target.ecsInstanceId, release.id]
- );
- return false;
- }
- const existing = await client.query(
- `SELECT release_id, status, attempt_count
- FROM extension_host_updates
- WHERE organization_id = $1 AND ecs_region_id = $2 AND ecs_instance_id = $3
- FOR UPDATE`,
- [input.organizationId, input.target.ecsRegionId, input.target.ecsInstanceId]
- );
- if (
- existing.rowCount
- && String(existing.rows[0]?.release_id) === release.id
- && String(existing.rows[0]?.status) === 'running'
- ) {
- return true;
- }
- if (
- existing.rowCount
- && String(existing.rows[0]?.release_id) === release.id
- && ['failed', 'waiting_for_idle'].includes(String(existing.rows[0]?.status))
- && Number(existing.rows[0]?.attempt_count) >= 3
- ) {
- return false;
- }
- await client.query(
- `INSERT INTO extension_host_updates
- (organization_id, ecs_region_id, ecs_instance_id, release_id, status, attempt_count, started_at)
- VALUES ($1, $2, $3, $4, 'running', 1, now())
- ON CONFLICT (organization_id, ecs_region_id, ecs_instance_id)
- DO UPDATE SET release_id = EXCLUDED.release_id, status = 'running',
- attempt_count = CASE
- WHEN extension_host_updates.release_id = EXCLUDED.release_id
- THEN LEAST(extension_host_updates.attempt_count + 1, 3)
- ELSE 1
- END,
- invocation_id = NULL,
- error_code = NULL, error_message = NULL, started_at = now(),
- deployed_at = NULL, verified_at = NULL, updated_at = now()`,
- [input.organizationId, input.target.ecsRegionId, input.target.ecsInstanceId, release.id]
- );
- return true;
- });
- }
-
- private async markVerified(input: ExtensionUpdateObservation, release: StoredExtensionRelease): Promise {
- await getPool(this.config).query(
- `UPDATE extension_host_updates
- SET status = 'verified', verified_at = now(), updated_at = now(),
- error_code = NULL, error_message = NULL
- WHERE organization_id = $1 AND ecs_region_id = $2 AND ecs_instance_id = $3
- AND release_id = $4 AND status IN ('waiting_for_idle', 'deployed', 'verified', 'failed')`,
- [input.organizationId, input.target.ecsRegionId, input.target.ecsInstanceId, release.id]
- );
- }
-
- private ensureUpdateRun(
- input: ExtensionUpdateObservation,
- release: StoredExtensionRelease,
- existingInvocationId?: string
- ): void {
- const regionId = String(input.target.ecsRegionId || '');
- const instanceId = String(input.target.ecsInstanceId || '');
- const key = hostKey({ organizationId: input.organizationId, regionId, instanceId });
- if (this.closed || this.inFlight.has(key)) return;
- const promise = this.runUpdate(input.organizationId, regionId, instanceId, release, existingInvocationId)
- .catch((error) => {
- const failure = sanitizedFailure(error);
- this.logger.error({
- diagnostic_event: 'extension.update.failed',
- diagnostic_stage: 'extension_update',
- ecs_region_id: regionId,
- ecs_instance_id: instanceId,
- target_version: release.version,
- error_code: failure.code
- }, 'extension host update failed');
- })
- .finally(() => this.inFlight.delete(key));
- this.inFlight.set(key, promise);
- }
-
- private async runUpdate(
- organizationId: string,
- regionId: string,
- instanceId: string,
- release: StoredExtensionRelease,
- existingInvocationId?: string
- ): Promise {
- let invocationId = existingInvocationId || '';
- try {
- const attempt = await getPool(this.config).query(
- `SELECT attempt_count, started_at
- FROM extension_host_updates
- WHERE organization_id = $1 AND ecs_region_id = $2 AND ecs_instance_id = $3
- AND release_id = $4 AND status = 'running'`,
- [organizationId, regionId, instanceId, release.id]
- );
- if (!attempt.rowCount) {
- throw definiteUpdateFailure(Object.assign(new Error('插件更新尝试记录不存在或已失效。'), {
- code: 'extension_update_attempt_missing'
- }));
- }
- const attemptCount = Number(attempt.rows[0]?.attempt_count);
- const startedAt = new Date(String(attempt.rows[0]?.started_at || '')).getTime();
- if (!Number.isSafeInteger(attemptCount) || attemptCount < 1 || !Number.isFinite(startedAt)) {
- throw definiteUpdateFailure(Object.assign(new Error('插件更新尝试记录无效。'), {
- code: 'extension_update_attempt_invalid'
- }));
- }
- const deadline = startedAt
- + this.config.EXTENSION_UPDATE_COMMAND_TIMEOUT_SECONDS * 1_000
- + UPDATE_COMMAND_RESULT_GRACE_MS;
- if (deadline <= Date.now()) {
- throw definiteUpdateFailure(Object.assign(
- new Error('等待云助手更新结果超时。'),
- { code: 'cloud_assistant_timeout' }
- ));
- }
- if (!invocationId) {
- // Both values are derived from the persisted attempt. If the service stops
- // after RunCommand succeeds but before invocation_id is stored, the next
- // heartbeat reconstructs byte-for-byte identical command parameters and
- // Alibaba Cloud can safely apply ClientToken idempotency.
- const expiresAt = deadline;
- if (expiresAt <= Date.now()) {
- throw definiteUpdateFailure(Object.assign(new Error('云助手命令受理状态在安全窗口内仍无法确定。'), {
- code: 'cloud_assistant_dispatch_timeout'
- }));
- }
- const token = createExtensionDownloadToken({
- releaseId: release.id,
- organizationId,
- regionId,
- instanceId,
- expiresAt
- }, this.config.fieldEncryptionKey);
- const downloadUrl = new URL(`/api/extension-updates/package/${encodeURIComponent(token)}`, this.config.APP_ORIGIN).toString();
- const script = buildExtensionUpdatePowerShell({
- downloadUrl,
- sha256: release.sha256,
- version: release.version,
- installPath: this.config.EXTENSION_WINDOWS_INSTALL_PATH,
- releaseId: release.id
- });
- let started: { invocationId: string };
- try {
- started = await this.commandClient.runPowerShell({
- regionId,
- instanceId,
- script,
- timeoutSeconds: this.config.EXTENSION_UPDATE_COMMAND_TIMEOUT_SECONDS,
- clientToken: createHash('sha256')
- .update(`${organizationId}:${regionId}:${instanceId}:${release.id}:${attemptCount}:${startedAt}`)
- .digest('hex')
- });
- } catch (error) {
- if (cloudApiDefinitelyRejected(error)) {
- throw definiteUpdateFailure(error instanceof Error ? error : new Error(String(error)));
- }
- throw error;
- }
- invocationId = started.invocationId;
- await getPool(this.config).query(
- `UPDATE extension_host_updates
- SET invocation_id = $1, updated_at = now()
- WHERE organization_id = $2 AND ecs_region_id = $3 AND ecs_instance_id = $4
- AND release_id = $5 AND status = 'running'`,
- [invocationId, organizationId, regionId, instanceId, release.id]
- );
- }
-
- while (!this.closed && Date.now() < deadline) {
- const result = await this.commandClient.getInvocation({ regionId, instanceId, invocationId });
- if (result.status === 'success') {
- await getPool(this.config).query(
- `UPDATE extension_host_updates
- SET status = 'deployed', deployed_at = now(), updated_at = now(),
- error_code = NULL, error_message = NULL
- WHERE organization_id = $1 AND ecs_region_id = $2 AND ecs_instance_id = $3
- AND release_id = $4 AND status = 'running'`,
- [organizationId, regionId, instanceId, release.id]
- );
- this.logger.info({
- diagnostic_event: 'extension.update.deployed',
- diagnostic_stage: 'extension_update',
- ecs_region_id: regionId,
- ecs_instance_id: instanceId,
- target_version: release.version
- }, 'extension package deployed; browser reload pending');
- return;
- }
- if (result.status === 'failed') {
- throw definiteUpdateFailure(Object.assign(new Error(result.errorMessage || '云助手命令执行失败。'), {
- code: result.errorCode || 'cloud_assistant_failed'
- }));
- }
- await delay(this.config.EXTENSION_UPDATE_POLL_INTERVAL_MS);
- }
- if (this.closed) return;
- throw definiteUpdateFailure(Object.assign(
- new Error('等待云助手更新结果超时。'),
- { code: 'cloud_assistant_timeout' }
- ));
- } catch (error) {
- const failure = sanitizedFailure(error);
- const candidate = error && typeof error === 'object' ? error as Record : {};
- if (candidate.extensionUpdateFailureDefinite !== true) {
- await getPool(this.config).query(
- `UPDATE extension_host_updates
- SET error_code = 'cloud_assistant_dispatch_uncertain', error_message = $1, updated_at = now()
- WHERE organization_id = $2 AND ecs_region_id = $3 AND ecs_instance_id = $4
- AND release_id = $5 AND status = 'running'`,
- [failure.message, organizationId, regionId, instanceId, release.id]
- ).catch(() => {});
- throw error;
- }
- await getPool(this.config).query(
- `UPDATE extension_host_updates
- SET status = 'failed', error_code = $1, error_message = $2, updated_at = now()
- WHERE organization_id = $3 AND ecs_region_id = $4 AND ecs_instance_id = $5
- AND release_id = $6 AND status = 'running'`,
- [failure.code, failure.message, organizationId, regionId, instanceId, release.id]
- ).catch(() => {});
- throw error;
- }
- }
-}
-
-export function createExtensionUpdateService(
- config: AppConfig,
- logger: DiagnosticLogger = noopDiagnosticLogger
-): ExtensionUpdateService {
- return new ExtensionUpdateService(
- config,
- new AliyunOssClient(config),
- new AliyunEcsCloudAssistantClient(config),
- logger
- );
-}
diff --git a/control-plane/src/oss-client.ts b/control-plane/src/oss-client.ts
index 28e8543..7ccfeb5 100644
--- a/control-plane/src/oss-client.ts
+++ b/control-plane/src/oss-client.ts
@@ -9,7 +9,6 @@ export interface OssPutObjectInput {
content: Buffer;
contentType: string;
contentDisposition?: string;
- acl?: 'private' | 'public-read';
}
export interface OssObjectClient {
@@ -169,7 +168,7 @@ export class AliyunOssClient implements OssObjectClient {
const headers: Record = {
'content-type': input.contentType,
'x-oss-content-sha256': UNSIGNED_PAYLOAD,
- 'x-oss-object-acl': input.acl || 'public-read'
+ 'x-oss-object-acl': 'public-read'
};
if (input.contentDisposition) headers['content-disposition'] = input.contentDisposition;
const additionalHeaders = input.contentDisposition ? ['content-disposition'] : [];
diff --git a/control-plane/src/server.ts b/control-plane/src/server.ts
index 3fc1155..82b7831 100644
--- a/control-plane/src/server.ts
+++ b/control-plane/src/server.ts
@@ -44,11 +44,6 @@ import {
normalizeRequestId,
writeEmergencyDiagnostic
} from './diagnostics.js';
-import {
- ExtensionUpdateError,
- createExtensionUpdateService,
- type ExtensionUpdateController
-} from './extension-updates.js';
export function aiServiceConnected(databaseIsReady: boolean, probe: Record): boolean {
return databaseIsReady && probe.configured === true && probe.reachable === true;
@@ -69,8 +64,6 @@ const accountCreateSchema = z.object({
password: z.string().min(1),
role: z.enum(['admin', 'team_lead', 'user']).default('user'),
erp_account: z.string().trim().max(200).optional(),
- extension_ecs_region_id: z.string().trim().max(64).optional(),
- extension_ecs_instance_id: z.string().trim().max(80).optional(),
business_route_ids: z.array(
z.string().trim().refine((routeId) => Boolean(businessRouteById(routeId)), '业务类型不存在。')
).max(BUSINESS_ROUTES.length).default([])
@@ -80,16 +73,8 @@ const accountCreateSchema = z.object({
const accountUpdateSchema = z.object({
role: z.enum(['admin', 'team_lead', 'user']).optional(),
is_active: z.boolean().optional(),
- erp_account: z.string().trim().max(200).nullable().optional(),
- extension_ecs_region_id: z.string().trim().max(64).nullable().optional(),
- extension_ecs_instance_id: z.string().trim().max(80).nullable().optional()
-}).refine((body) => (
- body.role !== undefined
- || body.is_active !== undefined
- || body.erp_account !== undefined
- || body.extension_ecs_region_id !== undefined
- || body.extension_ecs_instance_id !== undefined
-), {
+ erp_account: z.string().trim().max(200).nullable().optional()
+}).refine((body) => body.role !== undefined || body.is_active !== undefined || body.erp_account !== undefined, {
message: '至少提供一个账号更新字段。'
});
@@ -142,12 +127,8 @@ const heartbeatSchema = z.object({
extension_version: z.string().max(80).optional(),
erp_account: z.string().trim().max(200).optional(),
erp_account_matched: z.boolean().optional(),
- extension_update_safe: z.boolean().optional(),
metadata: z.record(z.unknown()).optional()
});
-const extensionReleasePublishSchema = z.object({
- package_base64: z.string().min(4).max(70_000_000)
-});
const automationSettingsSchema = z.object({ enabled: z.boolean() });
const parserRoutingUpdateSchema = z.object({
mode: z.enum(['ai', 'shadow', 'auto', 'program']),
@@ -415,14 +396,12 @@ export async function buildServer({
config = loadConfig(),
parser,
startParserLoop = true,
- loggerDestination,
- extensionUpdates: extensionUpdatesOverride
+ loggerDestination
}: {
config?: AppConfig;
parser?: ExternalParser;
startParserLoop?: boolean;
loggerDestination?: DestinationStream;
- extensionUpdates?: ExtensionUpdateController;
} = {}) {
const requestStartedAt = new WeakMap();
const app = Fastify({
@@ -430,11 +409,7 @@ export async function buildServer({
logController: new LogController({ disableRequestLogging: true }),
genReqId: (rawRequest) => normalizeRequestId(rawRequest.headers['x-request-id']),
trustProxy: true,
- bodyLimit: Math.min(75_000_000, Math.max(
- 2_000_000,
- Math.ceil(config.ARTIFACT_MAX_BYTES * 1.4) + 1_000_000,
- Math.ceil(config.EXTENSION_UPDATE_MAX_PACKAGE_BYTES * 1.4) + 1_000_000
- ))
+ bodyLimit: Math.min(75_000_000, Math.max(2_000_000, Math.ceil(config.ARTIFACT_MAX_BYTES * 1.4) + 1_000_000))
});
await app.register(cookie);
await app.register(helmet, { contentSecurityPolicy: false });
@@ -536,11 +511,6 @@ export async function buildServer({
warn: (metadata, message) => app.log.warn(metadata, message),
error: (metadata, message) => app.log.error(metadata, message)
});
- const extensionUpdates = extensionUpdatesOverride || createExtensionUpdateService(config, {
- info: (metadata, message) => app.log.info(metadata, message),
- warn: (metadata, message) => app.log.warn(metadata, message),
- error: (metadata, message) => app.log.error(metadata, message)
- });
const externalParser = parser || await loadExternalParser();
const parserOrchestrator = new ParserOrchestrator(externalParser);
const activeParseWorkers = new Map();
@@ -1013,8 +983,6 @@ export async function buildServer({
password: body.password,
role: body.role,
erpAccount: body.erp_account,
- extensionEcsRegionId: body.extension_ecs_region_id,
- extensionEcsInstanceId: body.extension_ecs_instance_id,
businessRouteIds: body.business_route_ids
}, requestId(request));
return { ok: true, account };
@@ -1028,9 +996,7 @@ export async function buildServer({
const account = await auth.updateAccount(session.user, userId, {
role: body.role,
isActive: body.is_active,
- erpAccount: body.erp_account,
- extensionEcsRegionId: body.extension_ecs_region_id,
- extensionEcsInstanceId: body.extension_ecs_instance_id
+ erpAccount: body.erp_account
}, requestId(request));
await agentBus?.reload();
return { ok: true, account };
@@ -1395,77 +1361,13 @@ export async function buildServer({
contextFor(session, request),
body.connection_id,
body.extension_version || '',
- {
- ...(body.metadata || {}),
- extension_update_safe: body.extension_update_safe === true
- },
+ body.metadata || {},
{
erpAccount: body.erp_account || '',
erpAccountMatched: body.erp_account_matched === true
}
);
- const { extension_update_target: updateTarget, ...publicWorker } = worker;
- const extensionUpdate = await extensionUpdates.observeHeartbeat({
- organizationId: session.user.organizationId,
- userId: session.user.id,
- connectionId: body.connection_id,
- currentVersion: body.extension_version || '',
- extensionUpdateSafe: body.extension_update_safe === true,
- target: {
- ecsRegionId: updateTarget.ecs_region_id,
- ecsInstanceId: updateTarget.ecs_instance_id,
- serverUpdateSafe: updateTarget.server_update_safe
- }
- });
- return { ok: true, connected: true, ...publicWorker, extension_update: extensionUpdate };
- });
-
- app.get('/api/extension-updates/releases', async (request) => {
- const session = await requireAdminSession(request);
- return {
- ok: true,
- enabled: config.EXTENSION_AUTO_UPDATE_ENABLED,
- releases: await extensionUpdates.listReleases(session.user.organizationId)
- };
- });
-
- app.post('/api/extension-updates/releases', {
- config: { rateLimit: { max: 5, timeWindow: '1 minute' } }
- }, async (request) => {
- const session = await requireAdminMutationSession(request);
- const body = extensionReleasePublishSchema.parse(request.body);
- const normalized = body.package_base64.replace(/\s+/gu, '');
- if (!/^[A-Za-z0-9+/]+={0,2}$/u.test(normalized)) {
- throw new ExtensionUpdateError('extension_package_base64_invalid', '插件包编码无效。');
- }
- const content = Buffer.from(normalized, 'base64');
- const canonical = content.toString('base64').replace(/=+$/u, '');
- if (canonical !== normalized.replace(/=+$/u, '')) {
- throw new ExtensionUpdateError('extension_package_base64_invalid', '插件包编码无效。');
- }
- return {
- ok: true,
- release: await extensionUpdates.publishRelease({
- organizationId: session.user.organizationId,
- actorUserId: session.user.id,
- requestId: requestId(request),
- content
- })
- };
- });
-
- app.get('/api/extension-updates/package/:token', {
- // A fleet may share one outbound NAT address and start together after a
- // release. The HMAC token is already release/organization/host/expiry
- // scoped, so keep only a generous abuse ceiling here.
- config: { rateLimit: { max: 300, timeWindow: '1 minute' } }
- }, async (request, reply) => {
- const params = request.params as { token: string };
- const download = await extensionUpdates.downloadPackage(params.token);
- reply.header('Cache-Control', 'private, no-store, max-age=0');
- reply.header('Content-Type', 'application/zip');
- reply.header('Content-Disposition', attachmentContentDisposition(download.fileName));
- return reply.send(download.content);
+ return { ok: true, connected: true, ...worker };
});
app.get('/api/audit', async (request) => {
@@ -1586,7 +1488,7 @@ export async function buildServer({
});
app.setErrorHandler((error, request, reply) => {
- if (error instanceof AuthError || error instanceof TaskError || error instanceof ExtensionUpdateError) {
+ if (error instanceof AuthError || error instanceof TaskError) {
request.log.warn({
diagnostic_event: 'http.request.rejected',
diagnostic_stage: 'http',
@@ -1633,14 +1535,13 @@ export async function buildServer({
}
app.addHook('onClose', async () => {
- extensionUpdates.close();
app.log.info({
diagnostic_event: 'service.closing',
diagnostic_stage: 'shutdown'
}, 'control plane closing');
await closePool();
});
- return { app, auth, tasks, agentBus, channelService, extensionUpdates };
+ return { app, auth, tasks, agentBus, channelService };
}
function installProcessDiagnostics(app: Awaited>['app']): void {
diff --git a/control-plane/src/task-service.ts b/control-plane/src/task-service.ts
index bfcd909..3ea6917 100644
--- a/control-plane/src/task-service.ts
+++ b/control-plane/src/task-service.ts
@@ -49,7 +49,6 @@ import {
createAgentBusAcceptedDeliveryPayload,
type AgentBusAcceptedDeliveryOptions
} from './agentbus-delivery.js';
-import { compareExtensionVersions } from './extension-updates.js';
export interface TaskEvent {
id: number;
@@ -2721,84 +2720,6 @@ export class TaskService {
}
}
- private async requireExtensionHostAvailable(
- client: import('pg').PoolClient,
- context: TaskContext,
- connectionId: string
- ): Promise {
- const update = await client.query(
- `SELECT host_update.status
- FROM users account
- JOIN extension_host_updates host_update
- ON host_update.organization_id = account.organization_id
- AND host_update.ecs_region_id = account.extension_ecs_region_id
- AND host_update.ecs_instance_id = account.extension_ecs_instance_id
- WHERE account.organization_id = $1
- AND account.id = $2
- AND host_update.status IN ('waiting_for_idle', 'running', 'deployed')
- FOR UPDATE OF host_update`,
- [context.organizationId, context.userId]
- );
- if (update.rowCount) {
- throw new TaskError(
- 'extension_host_update_in_progress',
- '当前云主机正在等待或执行插件更新;新 ERP 任务会在更新并验版后继续。',
- 409,
- { update_status: text(update.rows[0].status) }
- );
- }
- if (!this.config.EXTENSION_AUTO_UPDATE_ENABLED) return;
- const requiredVersion = await client.query(
- `SELECT release.version AS target_version,
- connection.extension_version AS current_version,
- account.extension_ecs_region_id,
- account.extension_ecs_instance_id,
- host_update.status AS update_status
- FROM extension_releases release
- JOIN browser_connections connection
- ON connection.organization_id = release.organization_id
- AND connection.user_id = $2
- AND connection.connection_id = $3
- JOIN users account
- ON account.organization_id = connection.organization_id
- AND account.id = connection.user_id
- LEFT JOIN extension_host_updates host_update
- ON host_update.organization_id = account.organization_id
- AND host_update.ecs_region_id = account.extension_ecs_region_id
- AND host_update.ecs_instance_id = account.extension_ecs_instance_id
- AND host_update.release_id = release.id
- WHERE release.organization_id = $1
- AND release.is_active = true`,
- [context.organizationId, context.userId, connectionId]
- );
- if (!requiredVersion.rowCount) return;
- const versionRow = requiredVersion.rows[0] as Record;
- const currentVersion = text(versionRow.current_version);
- const targetVersion = text(versionRow.target_version);
- let isCurrent = false;
- try {
- isCurrent = compareExtensionVersions(currentVersion, targetVersion) >= 0;
- } catch {
- isCurrent = false;
- }
- if (!isCurrent) {
- throw new TaskError(
- 'extension_update_required',
- `当前插件版本 ${currentVersion || '未知'} 尚未更新到 ${targetVersion};新 ERP 任务会在更新并验版后继续。`,
- 409,
- {
- current_version: currentVersion || null,
- target_version: targetVersion,
- update_status: text(versionRow.update_status) || (
- versionRow.extension_ecs_region_id && versionRow.extension_ecs_instance_id
- ? 'not_started'
- : 'unconfigured'
- )
- }
- );
- }
- }
-
private log(
level: 'info' | 'warn' | 'error',
metadata: Record,
@@ -6292,7 +6213,6 @@ export class TaskService {
}
await this.assertTaskCreatorBusinessAuthorizationInTransaction(client, row, context.requestId);
await this.requireBrowserConnection(client, context, connectionId);
- await this.requireExtensionHostAvailable(client, context, connectionId);
const existingAttempt = await client.query(
`SELECT id, status, response_hash
FROM task_attempts
@@ -7349,20 +7269,10 @@ export class TaskService {
extensionVersion: string,
metadata: Record = {},
routing: { erpAccount?: string; erpAccountMatched?: boolean } = {}
- ): Promise<{
- execution_ready: boolean;
- erp_account_matched: boolean;
- worker_connection_id: string;
- extension_update_target: {
- ecs_region_id: string | null;
- ecs_instance_id: string | null;
- server_update_safe: boolean;
- };
- }> {
+ ): Promise<{ execution_ready: boolean; erp_account_matched: boolean; worker_connection_id: string }> {
return withTransaction(this.config, async (client) => {
const account = await client.query(
- `SELECT id, role, is_active, erp_account,
- extension_ecs_region_id, extension_ecs_instance_id
+ `SELECT id, role, is_active, erp_account
FROM users
WHERE organization_id = $1 AND id = $2
FOR UPDATE`,
@@ -7448,59 +7358,10 @@ export class TaskService {
if (!result.rowCount) {
throw new TaskError('browser_connection_not_owned', '浏览器连接已绑定其他账号,请重新生成连接标识。', 403);
}
- const ecsRegionId = text(accountRow.extension_ecs_region_id) || null;
- const ecsInstanceId = text(accountRow.extension_ecs_instance_id) || null;
- let serverUpdateSafe = false;
- if (ecsRegionId && ecsInstanceId) {
- const unsafe = await client.query(
- `SELECT (
- EXISTS (
- SELECT 1
- FROM users host_account
- JOIN tasks task
- ON task.organization_id = host_account.organization_id
- AND task.assigned_user_id = host_account.id
- WHERE host_account.organization_id = $1
- AND host_account.extension_ecs_region_id = $2
- AND host_account.extension_ecs_instance_id = $3
- AND (
- task.status = 'reconciliation_pending'
- OR (task.lease_expires_at IS NOT NULL AND task.lease_expires_at > now())
- OR EXISTS (
- SELECT 1 FROM task_attempts attempt
- WHERE attempt.task_id = task.id
- AND attempt.phase = 'erp'
- AND attempt.status IN ('accepted', 'running', 'reconciliation_pending')
- )
- )
- )
- OR EXISTS (
- SELECT 1
- FROM users host_account
- JOIN browser_connections connection
- ON connection.organization_id = host_account.organization_id
- AND connection.user_id = host_account.id
- WHERE host_account.organization_id = $1
- AND host_account.extension_ecs_region_id = $2
- AND host_account.extension_ecs_instance_id = $3
- AND connection.status = 'connected'
- AND connection.last_seen_at >= now() - interval '90 seconds'
- AND lower(COALESCE(connection.metadata->>'extension_update_safe', 'false')) <> 'true'
- )
- ) AS unsafe`,
- [context.organizationId, ecsRegionId, ecsInstanceId]
- );
- serverUpdateSafe = !databaseBoolean(unsafe.rows[0]?.unsafe);
- }
return {
execution_ready: executionReady,
erp_account_matched: erpAccountMatched,
- worker_connection_id: connectionId,
- extension_update_target: {
- ecs_region_id: ecsRegionId,
- ecs_instance_id: ecsInstanceId,
- server_update_safe: serverUpdateSafe
- }
+ worker_connection_id: connectionId
};
});
}
diff --git a/control-plane/test/account-authorization.test.ts b/control-plane/test/account-authorization.test.ts
index 8f502cc..1b32451 100644
--- a/control-plane/test/account-authorization.test.ts
+++ b/control-plane/test/account-authorization.test.ts
@@ -71,37 +71,6 @@ test('AgentBus account-worker migration adds fail-closed channel, task, browser,
assert.match(sql, /owner_user_id IS NULL[\s\S]+enabled = true/);
});
-test('extension host migration binds accounts to ECS and persists release/update state without a host daemon', async () => {
- const [sql, server, tasks, extensionUpdates] = await Promise.all([
- source('../migrations/019_extension_host_updates.sql'),
- source('../src/server.ts'),
- source('../src/task-service.ts'),
- source('../src/extension-updates.ts')
- ]);
- assert.match(sql, /extension_ecs_region_id/);
- assert.match(sql, /extension_ecs_instance_id/);
- assert.match(sql, /CREATE TABLE IF NOT EXISTS extension_releases/);
- assert.match(sql, /CREATE TABLE IF NOT EXISTS extension_host_updates/);
- assert.match(sql, /waiting_for_idle/);
- assert.doesNotMatch(sql, /DELETE\s+FROM/i);
- assert.match(server, /\/api\/extension-updates\/releases/);
- assert.match(server, /extension_update_safe/);
- assert.match(tasks, /requireExtensionHostAvailable/);
- assert.match(tasks, /extension_update_required/);
- assert.match(tasks, /compareExtensionVersions/);
- assert.match(tasks, /attempt\.status IN \('accepted', 'running', 'reconciliation_pending'\)/);
- assert.match(extensionUpdates, /class AliyunEcsCloudAssistantClient/);
- assert.match(extensionUpdates, /type: 'RunPowerShellScript'/);
- assert.match(extensionUpdates, /Get-FileHash/);
- assert.match(extensionUpdates, /EXTENSION_WINDOWS_INSTALL_PATH/);
- assert.match(extensionUpdates, /安全完成已启动的上一版本部署/);
- assert.match(extensionUpdates, /id = \$2 AND is_active = true/);
- assert.match(extensionUpdates, /\$\{inspected\.sha256\}/);
- assert.match(extensionUpdates, /clientToken: createHash\('sha256'\)/);
- assert.doesNotMatch(extensionUpdates, /clientToken: randomUUID\(\)/);
- assert.doesNotMatch(extensionUpdates, /setInterval\(/);
-});
-
test('AgentBus channel keys and owners are unique so one inbound identity cannot fan out to multiple employees', async () => {
const channels = await source('../src/agentbus-channels.ts');
assert.match(channels, /requireAssignableOwner/);
diff --git a/control-plane/test/control-plane.test.ts b/control-plane/test/control-plane.test.ts
index e712b4a..9520376 100644
--- a/control-plane/test/control-plane.test.ts
+++ b/control-plane/test/control-plane.test.ts
@@ -317,7 +317,7 @@ test('control plane requires the latest durable task-outcome migration before re
const { readFile } = await import('node:fs/promises');
const db = await readFile(new URL('../src/db.ts', import.meta.url), 'utf8');
const server = await readFile(new URL('../src/server.ts', import.meta.url), 'utf8');
- assert.equal(REQUIRED_SCHEMA_VERSION, '019_extension_host_updates');
+ assert.equal(REQUIRED_SCHEMA_VERSION, '018_agentbus_account_workers');
assert.match(db, /schema_migrations/);
assert.match(db, /databaseReadiness/);
assert.match(db, /assertDatabaseSchema/);
@@ -1365,10 +1365,8 @@ test('operator page has a login gate and uses the durable task API', async () =>
assert.match(app, /window\.addEventListener\('focus',[\s\S]+refreshBackgroundState\(\)/);
assert.match(app, /async function autoDispatchReadyTasks\(\{ force = false \} = \{\}\)/);
assert.match(app, /if \(!force && retryAt > Date\.now\(\)\) continue/);
- assert.match(app, /extensionUpdateBlocksExecution = extensionUpdateIsBlocking\(extensionUpdate\)/);
- assert.match(app, /function extensionUpdateIsBlocking\(update = \{\}\) \{[\s\S]+!\['current', 'disabled', 'no_release'\]\.includes/);
- assert.match(app, /if \(!authUser \|\| autoHandoffInProgress \|\| extensionUpdateBlocksExecution\) return/);
- assert.match(app, /autoDispatchReadyTasks\(\{ force: true \}\)/);
+ assert.match(app, /const bridgeReadyForDispatch = !wasBridgeConnected/);
+ assert.match(app, /autoDispatchReadyTasks\(\{ force: bridgeReadyForDispatch \}\)/);
const autoDispatchSource = app.slice(
app.indexOf('async function autoDispatchReadyTasks'),
app.indexOf('function taskStateClass')
diff --git a/control-plane/test/diagnostics.test.ts b/control-plane/test/diagnostics.test.ts
index ace8f55..ff0b7cb 100644
--- a/control-plane/test/diagnostics.test.ts
+++ b/control-plane/test/diagnostics.test.ts
@@ -65,10 +65,6 @@ test('diagnostic request identifiers and paths are stable and query-safe', () =>
assert.match(generated, /^[a-f0-9-]{36}$/u);
assert.doesNotMatch(generated, /token/u);
assert.equal(diagnosticRequestPath('/api/tasks/TASK-1?token=secret#fragment'), '/api/tasks/TASK-1');
- assert.equal(
- diagnosticRequestPath('/api/extension-updates/package/secret-bearer-token'),
- '/api/extension-updates/package/:token'
- );
const startedAt = process.hrtime.bigint() - 2_000_000n;
assert.ok(diagnosticDurationMs(startedAt) >= 1);
});
diff --git a/control-plane/test/extension-updates.test.ts b/control-plane/test/extension-updates.test.ts
deleted file mode 100644
index daa1868..0000000
--- a/control-plane/test/extension-updates.test.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import assert from 'node:assert/strict';
-import { createHash } from 'node:crypto';
-import test from 'node:test';
-import JSZip from 'jszip';
-import { loadConfig } from '../src/config.js';
-import {
- ExtensionUpdateError,
- buildExtensionUpdatePowerShell,
- compareExtensionVersions,
- createExtensionDownloadToken,
- inspectExtensionPackage,
- interpretCloudAssistantInvocation,
- verifyExtensionDownloadToken
-} from '../src/extension-updates.js';
-
-async function extensionZip(version = '0.5.167'): Promise {
- const zip = new JSZip();
- zip.file('manifest.json', JSON.stringify({
- manifest_version: 3,
- name: '联泰下单助手',
- version,
- background: { service_worker: 'background.js' },
- content_scripts: [{ matches: ['https://business.example.test/*'], js: ['business-bridge.js'] }]
- }));
- zip.file('background.js', 'chrome.runtime.onMessage.addListener(() => {});');
- zip.file('business-bridge.js', 'window.postMessage({ ok: true });');
- return zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
-}
-
-test('extension versions compare numerically rather than lexically', () => {
- assert.equal(compareExtensionVersions('0.5.167', '0.5.166'), 1);
- assert.equal(compareExtensionVersions('0.10.0', '0.9.99'), 1);
- assert.equal(compareExtensionVersions('1.0.0', '1.0.0.0'), 0);
- assert.equal(compareExtensionVersions('0.5.166', '0.5.167'), -1);
- assert.throws(
- () => compareExtensionVersions('latest', '0.5.167'),
- (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_version_invalid'
- );
-});
-
-test('extension release inspection binds identity, version, required files, and SHA-256', async () => {
- const content = await extensionZip();
- const inspected = await inspectExtensionPackage(content);
- assert.equal(inspected.version, '0.5.167');
- assert.equal(inspected.manifest.name, '联泰下单助手');
- assert.equal(inspected.entryCount, 3);
- assert.equal(inspected.sha256, createHash('sha256').update(content).digest('hex'));
-
- const invalid = new JSZip();
- invalid.file('manifest.json', JSON.stringify({
- manifest_version: 3,
- name: '其他插件',
- version: '0.5.167',
- background: { service_worker: 'background.js' },
- content_scripts: [{ matches: ['https://business.example.test/*'], js: ['business-bridge.js'] }]
- }));
- invalid.file('background.js', '');
- invalid.file('business-bridge.js', '');
- await assert.rejects(
- inspectExtensionPackage(await invalid.generateAsync({ type: 'nodebuffer' })),
- (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_manifest_identity_mismatch'
- );
-
- const unsafePath = await extensionZip();
- const unsafeZip = await JSZip.loadAsync(unsafePath);
- unsafeZip.file('asset.js:alternate-stream', 'forbidden');
- await assert.rejects(
- inspectExtensionPackage(await unsafeZip.generateAsync({ type: 'nodebuffer' })),
- (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_package_path_invalid'
- );
-});
-
-test('short-lived host-scoped package tokens reject tampering and expiry', () => {
- const key = Buffer.alloc(32, 7);
- const payload = {
- releaseId: 'release-a',
- organizationId: 'organization-a',
- regionId: 'cn-hangzhou',
- instanceId: 'i-12345678',
- expiresAt: 2_000
- };
- const token = createExtensionDownloadToken(payload, key);
- assert.deepEqual(verifyExtensionDownloadToken(token, key, 1_999), payload);
- assert.throws(
- () => verifyExtensionDownloadToken(`${token}x`, key, 1_999),
- (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_download_token_invalid'
- );
- assert.throws(
- () => verifyExtensionDownloadToken(token, key, 2_001),
- (error: unknown) => error instanceof ExtensionUpdateError && error.code === 'extension_download_token_expired'
- );
-});
-
-test('PowerShell updater carries encoded values, enforces ProgramData, hash, staging, and rollback', () => {
- const script = buildExtensionUpdatePowerShell({
- downloadUrl: 'https://business.example.test/api/extension-updates/package/token',
- sha256: 'a'.repeat(64),
- version: '0.5.167',
- installPath: 'C:\\ProgramData\\LTJT\\chrome-extension\\ltjt-order-assistant',
- releaseId: 'release-a'
- });
- assert.match(script, /Get-FileHash/);
- assert.match(script, /PackageHashMismatch/);
- assert.match(script, /InstallPathOutsideAllowedRoot/);
- assert.match(script, /LTJT_EXTENSION_NEWER_PRESENT/);
- assert.match(script, /\.previous/);
- assert.match(script, /Move-Item -LiteralPath \$backupPath -Destination \$installPath/);
- assert.ok(Buffer.byteLength(Buffer.from(script, 'utf8').toString('base64')) < 24 * 1024);
- assert.doesNotMatch(script, /business\.example\.test/);
-});
-
-test('Cloud Assistant success requires exit zero and the updater completion marker', () => {
- assert.deepEqual(
- interpretCloudAssistantInvocation({
- invocationStatus: 'Success',
- exitCode: 0,
- output: 'LTJT_EXTENSION_UPDATED 0.5.167\n'
- }),
- {
- status: 'success',
- exitCode: 0,
- output: 'LTJT_EXTENSION_UPDATED 0.5.167\n'
- }
- );
- assert.equal(
- interpretCloudAssistantInvocation({ invocationStatus: 'Success', exitCode: 0, output: '' }).errorCode,
- 'extension_update_marker_missing'
- );
- assert.equal(
- interpretCloudAssistantInvocation({ invocationStatus: 'Running', output: '' }).status,
- 'running'
- );
- assert.equal(
- interpretCloudAssistantInvocation({
- invocationStatus: 'Success',
- exitCode: 0,
- output: 'LTJT_EXTENSION_NEWER_PRESENT 0.5.168\r\n'
- }).status,
- 'success'
- );
- assert.equal(
- interpretCloudAssistantInvocation({ invocationStatus: 'Aborted', errorCode: 'ClientNotRunning' }).status,
- 'failed'
- );
-});
-
-test('extension updater is off by default and enabled production config requires HTTPS plus OSS and ECS credentials', () => {
- const disabled = loadConfig({ NODE_ENV: 'test' });
- assert.equal(disabled.EXTENSION_AUTO_UPDATE_ENABLED, false);
- assert.equal(disabled.EXTENSION_WINDOWS_INSTALL_PATH, 'C:\\ProgramData\\LTJT\\chrome-extension\\ltjt-order-assistant');
- assert.throws(() => loadConfig({
- NODE_ENV: 'production',
- FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 1).toString('base64'),
- APP_ORIGIN: 'https://business.example.test',
- EXTENSION_AUTO_UPDATE_ENABLED: 'true'
- }), /missing configuration/);
- assert.throws(() => loadConfig({
- NODE_ENV: 'production',
- FIELD_ENCRYPTION_KEY: Buffer.alloc(32, 1).toString('base64'),
- APP_ORIGIN: 'http://business.example.test',
- EXTENSION_AUTO_UPDATE_ENABLED: 'true',
- OSS_ACCESS_KEY_ID: 'oss-id',
- OSS_ACCESS_KEY_SECRET: 'oss-secret',
- OSS_ENDPOINT: 'oss-cn-hangzhou.aliyuncs.com',
- OSS_BUCKET_NAME: 'bucket',
- OSS_REGION: 'cn-hangzhou',
- ALIBABA_CLOUD_ACCESS_KEY_ID: 'ecs-id',
- ALIBABA_CLOUD_ACCESS_KEY_SECRET: 'ecs-secret'
- }), /must use HTTPS/);
-});
diff --git a/archive/releases/2026-09-03/ltjt-order-assistant-0.5.165.zip b/dist/ltjt-order-assistant-0.5.165.zip
similarity index 100%
rename from archive/releases/2026-09-03/ltjt-order-assistant-0.5.165.zip
rename to dist/ltjt-order-assistant-0.5.165.zip
diff --git a/dist/ltjt-order-assistant-0.5.167.zip b/dist/ltjt-order-assistant-0.5.167.zip
deleted file mode 100644
index 77b51cf..0000000
Binary files a/dist/ltjt-order-assistant-0.5.167.zip and /dev/null differ
diff --git a/dist/release-manifest.json b/dist/release-manifest.json
index a67efa6..bfdd98d 100644
--- a/dist/release-manifest.json
+++ b/dist/release-manifest.json
@@ -1,8 +1,8 @@
{
"manifest_version": 1,
- "generated_on": "2026-09-03",
+ "generated_on": "2026-09-02",
"baselines": {
- "chrome_extension": "0.5.167",
+ "chrome_extension": "0.5.165",
"skills": "0.5.125",
"business_instruction_docx": "0.5.125",
"agent_prompt": "ltjt-agent-prompt-v1.8-independent-headcount-categories"
@@ -10,10 +10,10 @@
"artifacts": [
{
"kind": "chrome_extension",
- "version": "0.5.167",
- "path": "dist/ltjt-order-assistant-0.5.167.zip",
+ "version": "0.5.165",
+ "path": "dist/ltjt-order-assistant-0.5.165.zip",
"source": "chrome-extension/ltjt-order-assistant",
- "sha256": "6822ec660097a8889da8ea4fa22d4091467aa022067bff3bee2ac5406864b679"
+ "sha256": "14f3150ab26d99131e32321ddc805a85550438bc2810a1e20921f7922b25aaac"
},
{
"kind": "skill",
diff --git a/mappings/lifecycle.mapping.json b/mappings/lifecycle.mapping.json
index 68e11b4..3cd3c15 100644
--- a/mappings/lifecycle.mapping.json
+++ b/mappings/lifecycle.mapping.json
@@ -1,7 +1,7 @@
{
"contract_version": "ltjt-lifecycle-v2.9-roster-leader-contact-2026-08",
"current_agent_parse_prompt_version": "ltjt-agent-prompt-v1.8-independent-headcount-categories",
- "current_extension_version": "0.5.167",
+ "current_extension_version": "0.5.165",
"historical_test_marker": "TEST-202609",
"scope": "/System/Business/",
"erp_session_keepalive": {
diff --git a/mappings/plan_add.mapping.json b/mappings/plan_add.mapping.json
index 2281cb3..ecb4cb9 100644
--- a/mappings/plan_add.mapping.json
+++ b/mappings/plan_add.mapping.json
@@ -1,7 +1,7 @@
{
"$schema": "https://local.lwltapi/schemas/ltjt_form_mapping.schema.json",
- "mapping_version": "2026-09-03.shared_plan_create.plan_add.v0.10",
- "status": "adaptive_list_entry_readiness_release_gated_0.5.167",
+ "mapping_version": "2026-09-02.shared_plan_create.plan_add.v0.9",
+ "status": "native_nonempty_product_search_live_verified_release_gated_0.5.165",
"source": {
"schema": "schemas/standard_system_operation.schema.json",
"action": "shared_plan_create"
@@ -16,14 +16,6 @@
"payload_prefix": "Act=DoInfoSPs&",
"response_type": "script"
},
- "entry_readiness": {
- "page": "/System/Business/plan.asp",
- "target": "#NewBuildB, [name=\"NewBuildB\"]",
- "policy": "check immediately; while absent reacquire the current main-iframe document and poll every 100ms; click as soon as the button is connected and enabled",
- "timeout_ms": 15000,
- "fixed_delay": false,
- "failure_policy": "block before click and before ERP write"
- },
"ordered_steps": [
{ "step": 1, "operation": "set_date_range", "source": "data.recurrence.start_date/end_date", "targets": ["Riqi1", "Riqi2"] },
{ "step": 2, "operation": "resolve_and_select_product", "source": "data.product.keyword || data.product.name", "search_target": "S_chanpinming", "radio_target": "cp_id", "side_effect": "set_th fills tuanxuhao1/tuanxuhao2", "unique_required": true, "fallback": "match loaded candidates first; if unresolved, issue the native non-empty product-name query and match the returned name/duration fields locally; strip only a trailing numeric duration before the native query; retain one empty-query compatibility reload; never choose the first row" },
diff --git a/package-lock.json b/package-lock.json
index 71b16db..92df8be 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,6 @@
"name": "liansyn-platform",
"version": "1.0.0",
"dependencies": {
- "@alicloud/ecs20140526": "^7.11.1",
- "@alicloud/openapi-core": "^1.0.8",
"@fastify/cookie": "^10.0.1",
"@fastify/helmet": "^13.0.1",
"@fastify/rate-limit": "^10.2.2",
@@ -38,192 +36,6 @@
"node": ">=22"
}
},
- "node_modules/@alicloud/credentials": {
- "version": "2.4.7",
- "resolved": "https://registry.npmjs.org/@alicloud/credentials/-/credentials-2.4.7.tgz",
- "integrity": "sha512-pVeLM4Mjdh2PSs+h36CDBOCsvXyZTBG3tgT7chzH8jqWgVGU4UIgYw2h76ZviBpqADFPs3QReYg2AX/gzmqwOw==",
- "license": "MIT",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.8.0",
- "httpx": "^2.3.3",
- "ini": "^1.3.5",
- "kitx": "^2.0.0"
- }
- },
- "node_modules/@alicloud/darabonba-array": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/@alicloud/darabonba-array/-/darabonba-array-0.1.2.tgz",
- "integrity": "sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ==",
- "license": "ISC",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.7.1"
- }
- },
- "node_modules/@alicloud/darabonba-encode-util": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz",
- "integrity": "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==",
- "license": "ISC",
- "dependencies": {
- "moment": "^2.29.1"
- }
- },
- "node_modules/@alicloud/darabonba-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz",
- "integrity": "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==",
- "license": "ISC",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.7.1"
- }
- },
- "node_modules/@alicloud/darabonba-signature-util": {
- "version": "0.0.4",
- "resolved": "https://registry.npmjs.org/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz",
- "integrity": "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==",
- "license": "ISC",
- "dependencies": {
- "@alicloud/darabonba-encode-util": "^0.0.1"
- }
- },
- "node_modules/@alicloud/darabonba-signature-util/node_modules/@alicloud/darabonba-encode-util": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz",
- "integrity": "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==",
- "license": "ISC",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.7.1",
- "moment": "^2.29.1"
- }
- },
- "node_modules/@alicloud/darabonba-string": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz",
- "integrity": "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.5.1"
- }
- },
- "node_modules/@alicloud/ecs20140526": {
- "version": "7.11.1",
- "resolved": "https://registry.npmjs.org/@alicloud/ecs20140526/-/ecs20140526-7.11.1.tgz",
- "integrity": "sha512-d8iTPH4hBXmMPOZQon7oroZGsj4DxZHY2ta3kbSqumBMFYHnRI7SJNTGErQQC8tZgKNo9OOdHb7M64MyQf7erw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@alicloud/openapi-core": "^1.0.8",
- "@darabonba/typescript": "^1.0.0"
- }
- },
- "node_modules/@alicloud/endpoint-util": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz",
- "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.5.1",
- "kitx": "^2.0.0"
- }
- },
- "node_modules/@alicloud/gateway-pop": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz",
- "integrity": "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==",
- "license": "ISC",
- "dependencies": {
- "@alicloud/credentials": "^2",
- "@alicloud/darabonba-array": "^0.1.0",
- "@alicloud/darabonba-encode-util": "^0.0.2",
- "@alicloud/darabonba-map": "^0.0.1",
- "@alicloud/darabonba-signature-util": "^0.0.4",
- "@alicloud/darabonba-string": "^1.0.2",
- "@alicloud/endpoint-util": "^0.0.1",
- "@alicloud/gateway-spi": "^0.0.8",
- "@alicloud/openapi-util": "^0.3.2",
- "@alicloud/tea-typescript": "^1.7.1",
- "@alicloud/tea-util": "^1.4.8"
- }
- },
- "node_modules/@alicloud/gateway-spi": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz",
- "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==",
- "license": "ISC",
- "dependencies": {
- "@alicloud/credentials": "^2",
- "@alicloud/tea-typescript": "^1.7.1"
- }
- },
- "node_modules/@alicloud/openapi-core": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@alicloud/openapi-core/-/openapi-core-1.0.8.tgz",
- "integrity": "sha512-xs8LdgMDcEUqv13kZ4nl+Vd+Fc1mixTF0g1lm5QSrNWDaLUUD5vqpuMtvUZnKNnq9PITfUQ+8KunAvNQJpFo8g==",
- "hasInstallScript": true,
- "license": "ISC",
- "dependencies": {
- "@alicloud/credentials": "^2.4.2",
- "@alicloud/gateway-pop": "0.0.6",
- "@alicloud/gateway-spi": "^0.0.8",
- "@darabonba/typescript": "^1.0.5"
- }
- },
- "node_modules/@alicloud/openapi-util": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/@alicloud/openapi-util/-/openapi-util-0.3.3.tgz",
- "integrity": "sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A==",
- "license": "ISC",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.7.1",
- "@alicloud/tea-util": "^1.3.0",
- "kitx": "^2.1.0",
- "sm3": "^1.0.3"
- }
- },
- "node_modules/@alicloud/tea-typescript": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz",
- "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==",
- "license": "ISC",
- "dependencies": {
- "@types/node": "^12.0.2",
- "httpx": "^2.2.6"
- }
- },
- "node_modules/@alicloud/tea-typescript/node_modules/@types/node": {
- "version": "12.20.55",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
- "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
- "license": "MIT"
- },
- "node_modules/@alicloud/tea-util": {
- "version": "1.4.11",
- "resolved": "https://registry.npmjs.org/@alicloud/tea-util/-/tea-util-1.4.11.tgz",
- "integrity": "sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww==",
- "license": "Apache-2.0",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.5.1",
- "@darabonba/typescript": "^1.0.0",
- "kitx": "^2.0.0"
- }
- },
- "node_modules/@darabonba/typescript": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@darabonba/typescript/-/typescript-1.0.5.tgz",
- "integrity": "sha512-pfxHFVM8I3h8K2o8skpDQLMmR5iAeQ2eNpP1HrKDEm/9ZPF8aKwDKPwwEszT1NnIo0Y3++E7x1YsVkVpGjA/xw==",
- "license": "Apache License 2.0",
- "dependencies": {
- "@alicloud/tea-typescript": "^1.5.1",
- "http-proxy-agent": "^5.0.0",
- "https-proxy-agent": "^5.0.1",
- "httpx": "^2.3.2",
- "lodash": "^4.17.21",
- "moment": "^2.30.1",
- "moment-timezone": "^0.5.45",
- "socks-proxy-agent": "^6.2.1",
- "ws": "^8.18.0",
- "xml2js": "^0.6.2"
- }
- },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
@@ -989,19 +801,11 @@
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT"
},
- "node_modules/@tootallnate/once": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz",
- "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@types/node": {
"version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -1045,18 +849,6 @@
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
"license": "MIT"
},
- "node_modules/agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "4"
- },
- "engines": {
- "node": ">= 6.0.0"
- }
- },
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
@@ -1493,23 +1285,6 @@
"integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
"license": "MIT"
},
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -1904,52 +1679,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/http-proxy-agent": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
- "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
- "license": "MIT",
- "dependencies": {
- "@tootallnate/once": "2",
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/https-proxy-agent": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
- "license": "MIT",
- "dependencies": {
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/httpx": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.3.tgz",
- "integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "^20",
- "debug": "^4.1.1"
- }
- },
- "node_modules/httpx/node_modules/@types/node": {
- "version": "20.19.43",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
- "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
- }
- },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -1993,21 +1722,6 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
- },
- "node_modules/ip-address": {
- "version": "10.7.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
- "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/ipaddr.js": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
@@ -2090,15 +1804,6 @@
"safe-buffer": "~5.1.0"
}
},
- "node_modules/kitx": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/kitx/-/kitx-2.2.0.tgz",
- "integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "^22.5.4"
- }
- },
"node_modules/lazystream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
@@ -2193,12 +1898,6 @@
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==",
"license": "ISC"
},
- "node_modules/lodash": {
- "version": "4.18.1",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
- "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
- "license": "MIT"
- },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -2344,33 +2043,6 @@
"mkdirp": "bin/cmd.js"
}
},
- "node_modules/moment": {
- "version": "2.30.1",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
- "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/moment-timezone": {
- "version": "0.5.48",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
- "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
- "license": "MIT",
- "dependencies": {
- "moment": "^2.29.4"
- },
- "engines": {
- "node": "*"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
"node_modules/node-addon-api": {
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz",
@@ -2854,15 +2526,6 @@
"node": ">=10"
}
},
- "node_modules/sax": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
- "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=11.0.0"
- }
- },
"node_modules/saxes": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@@ -2921,50 +2584,6 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
- "node_modules/sm3": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sm3/-/sm3-1.0.3.tgz",
- "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==",
- "license": "MIT"
- },
- "node_modules/smart-buffer": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
- "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
- "license": "MIT",
- "engines": {
- "node": ">= 6.0.0",
- "npm": ">= 3.0.0"
- }
- },
- "node_modules/socks": {
- "version": "2.8.10",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz",
- "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==",
- "license": "MIT",
- "dependencies": {
- "ip-address": "^10.1.1",
- "smart-buffer": "^4.2.0"
- },
- "engines": {
- "node": ">= 10.0.0",
- "npm": ">= 3.0.0"
- }
- },
- "node_modules/socks-proxy-agent": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz",
- "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==",
- "license": "MIT",
- "dependencies": {
- "agent-base": "^6.0.2",
- "debug": "^4.3.3",
- "socks": "^2.6.2"
- },
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
@@ -3099,6 +2718,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
"license": "MIT"
},
"node_modules/unzipper": {
@@ -3180,28 +2800,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/xml2js": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
- "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
- "license": "MIT",
- "dependencies": {
- "sax": ">=0.6.0",
- "xmlbuilder": "~11.0.0"
- },
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/xmlbuilder": {
- "version": "11.0.1",
- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
- "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
diff --git a/package.json b/package.json
index df6fd87..75de2d0 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,6 @@
"test": "node --run check && node --run test:control-plane && node --run test:legacy"
},
"dependencies": {
- "@alicloud/ecs20140526": "^7.11.1",
- "@alicloud/openapi-core": "^1.0.8",
"@fastify/cookie": "^10.0.1",
"@fastify/helmet": "^13.0.1",
"@fastify/rate-limit": "^10.2.2",
diff --git a/tools/lifecycle-contract.test.mjs b/tools/lifecycle-contract.test.mjs
index aa72f42..13052bc 100644
--- a/tools/lifecycle-contract.test.mjs
+++ b/tools/lifecycle-contract.test.mjs
@@ -307,28 +307,6 @@ test('team-order live gate allows numbered receivable drift like 0.5.156', async
clearTimeout(jq.ajax.__ltjt_restore_timer);
});
-test('split-parent entry waits adaptively for the current iframe add button before clicking', async () => {
- const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
- const readinessBody = inpage.slice(
- inpage.indexOf('async function waitForSplitParentAddButton'),
- inpage.indexOf('function nativeListRequestMatches')
- );
- const createBody = inpage.slice(
- inpage.indexOf('async function createSplitParentLive'),
- inpage.indexOf('async function createSplitChildLive')
- );
- assert.match(inpage, /SPLIT_PARENT_ENTRY_READY_TIMEOUT_MS = 15000/);
- assert.match(inpage, /SPLIT_PARENT_ENTRY_READY_INTERVAL_MS = 100/);
- assert.match(readinessBody, /return waitForCondition/);
- assert.match(readinessBody, /const candidate = mainIframe\(\) \? mainIframeDocument\(\) : initialListDocument/);
- assert.match(readinessBody, /addButton\.isConnected === false/);
- assert.match(readinessBody, /addButton\.disabled === true/);
- assert.match(createBody, /const listReady = await waitForSplitParentAddButton\(listDocument\)/);
- assert.match(createBody, /listDocument = listReady\.document/);
- assert.match(createBody, /listReady\.addButton\.click\(\)/);
- assert.doesNotMatch(createBody, /const addButton = listDocument\.querySelector/);
-});
-
test('split-parent post-write verification retries read-only and falls back after any missed response group', async () => {
const inpage = await readFile(new URL('../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8');
const createBody = inpage.slice(
@@ -1318,7 +1296,7 @@ test('passenger explicit server success is terminal without a post-save row requ
assert.doesNotMatch(passengerBranch, /passengerRequery|verifyLifecycleOperation/);
assert.doesNotMatch(background, /attemptAutomaticPassengerReconciliation/);
assert.match(background, /名单已取得 ERP 明确成功响应,按业务规则确认录入成功/);
- assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.167'/);
+ assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.165'/);
});
test('uncertain lifecycle writes can only converge through a read-only plugin requery', async () => {
@@ -1525,25 +1503,6 @@ test('lifecycle execution keeps the MV3 worker alive until the durable task sett
assert.match(background, /arrangement_resource_candidate_data_missing/);
});
-test('extension update reload requires a fully idle durable execution state', async () => {
- const [background, bridge, platform] = await Promise.all([
- readFile(new URL('../chrome-extension/ltjt-order-assistant/background.js', import.meta.url), 'utf8'),
- readFile(new URL('../chrome-extension/ltjt-order-assistant/business-bridge.js', import.meta.url), 'utf8'),
- readFile(new URL('../LianSyn-platform/app.js', import.meta.url), 'utf8')
- ]);
- assert.match(background, /async function extensionUpdateSafetyStatus\(\)/);
- assert.match(background, /\['running', 'write_started', 'submitted', 'uncertain'\]/);
- assert.match(background, /saved_unverified\|execution_uncertain\|reconciliation_pending/);
- assert.match(background, /message\?\.type === 'LTJT_EXTENSION_UPDATE_STATUS'/);
- assert.match(background, /message\?\.type === 'LTJT_APPLY_EXTENSION_UPDATE'/);
- assert.match(background, /if \(comparison >= 0\)/);
- assert.match(background, /chrome\.runtime\.reload\(\)/);
- assert.match(bridge, /message\.type === 'APPLY_EXTENSION_UPDATE'/);
- assert.match(bridge, /type: 'LTJT_APPLY_EXTENSION_UPDATE'/);
- assert.match(platform, /extension_update_safe: result\.extension_update\?\.safe === true/);
- assert.match(platform, /extensionUpdateBlocksExecution/);
-});
-
test('ERP browser injection is host-permission gated and fail-closed with diagnostics', async () => {
const [manifestText, background, platform] = await Promise.all([
readFile(new URL('../chrome-extension/ltjt-order-assistant/manifest.json', import.meta.url), 'utf8'),
@@ -1967,7 +1926,7 @@ test('delete guard distinguishes independent, shared child, and shared parent ro
for (const operation of operations) assert.equal(plans.validateOperation(operation).ok, true, plans.validateOperation(operation).blockers.join('; '));
const mapping = JSON.parse(await readFile(new URL('../mappings/lifecycle.mapping.json', import.meta.url), 'utf8'));
- assert.equal(mapping.current_extension_version, '0.5.167');
+ assert.equal(mapping.current_extension_version, '0.5.165');
assert.equal(
mapping.updates.order_update_independent.field_mapped_pending_live_validation['pax.child_no_bed'],
'ertrenshu'
@@ -2350,10 +2309,10 @@ test('schema and browser adapters contain the v2 safety fields and no confirm ov
assert.match(inpage, /`ys_danweiid\$\{index\}`, resolvedCustomerId/);
assert.doesNotMatch(inpage, /product_customer_source_region|sourceRegionCheck|source_reference/);
assert.doesNotMatch(teamBatchInpage, /product_customer_source_region|sourceRegionCheck/);
- assert.equal(extensionManifest.version, '0.5.167');
- assert.match(inpage, /version: '0\.5\.167'/);
- assert.match(teamBatchInpage, /version: '0\.5\.167'/);
- assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.167'/);
+ assert.equal(extensionManifest.version, '0.5.165');
+ assert.match(inpage, /version: '0\.5\.165'/);
+ assert.match(teamBatchInpage, /version: '0\.5\.165'/);
+ assert.match(platformApp, /REQUIRED_EXTENSION_VERSION = '0\.5\.165'/);
assert.match(inpage, /function strictIsoDate\(value\)/);
assert.match(inpage, /const startDate = strictIsoDate\(controlCanonicalValue\(form, 'riqi0'\)\)/);
assert.match(inpage, /const endDate = strictIsoDate\(controlCanonicalValue\(form, 'riqis0'\)\)/);