@whyshu/argent-engine 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MANIFEST.json +46 -40
- package/engine/VERSION +1 -1
- package/engine/scripts/appointment-create.mjs +34 -0
- package/engine/scripts/appointment-stop.mjs +18 -0
- package/engine/scripts/appointment-worker.mjs +96 -0
- package/engine/scripts/appointment_grab.py +327 -0
- package/engine/scripts/argent-runner.mjs +223 -0
- package/engine/scripts/argent-setup-check.mjs +3 -3
- package/engine/scripts/capability-packs.lock.json +34 -40
- package/engine/scripts/check-agent-catalog.mjs +1 -1
- package/engine/scripts/check-config-catalog.mjs +0 -1
- package/engine/scripts/collect-evidence.mjs +2 -5
- package/engine/scripts/daily-report.mjs +315 -12
- package/engine/scripts/drill-clean-room.sh +45 -0
- package/engine/scripts/drill-handover.sh +4 -1
- package/engine/scripts/dsh-upgrade.mjs +16 -20
- package/engine/scripts/flow-requirements.json +5 -7
- package/engine/scripts/flow-timers.sh +1 -1
- package/engine/scripts/flow-trigger.sh +1 -1
- package/engine/scripts/gen-preset-rows.mjs +20 -0
- package/engine/scripts/handover-sheet.mjs +9 -1
- package/engine/scripts/invoice_stageb.py +20 -1
- package/engine/scripts/lib/agent-install.mjs +10 -1
- package/engine/scripts/lib/daily-report-core.mjs +124 -16
- package/engine/scripts/lib/dsh-versions.mjs +17 -1
- package/engine/scripts/lib/ml-ads-core.mjs +204 -0
- package/engine/scripts/lib/plugin-packages.mjs +1 -0
- package/engine/scripts/ml-ads.mjs +181 -0
- package/engine/scripts/ml-oauth-setup.mjs +1 -1
- package/engine/scripts/orchestrator.py +91 -10
- package/engine/scripts/p2-report-run.mjs +102 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +34 -9
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +32 -8
- package/engine/scripts/patches/retire-check.sh +0 -4
- package/engine/scripts/patches/verify-hardening.mjs +4 -1
- package/engine/scripts/patches/verify-turn-outside-replace.mjs +64 -0
- package/engine/scripts/poll-fulfillment.sh +1 -1
- package/engine/scripts/profile-doctor.mjs +1 -1
- package/engine/scripts/python-runtime.sh +16 -4
- package/engine/scripts/release-argent.sh +8 -7
- package/engine/scripts/session-handoff.mjs +111 -0
- package/engine/scripts/session-load-check.mjs +105 -0
- package/engine/scripts/sync-scripts.sh +1 -1
- package/engine/scripts/xzh-browser.sh +37 -0
- package/engine/scripts/xzh_browser.py +49 -0
- package/engine/skills/ml-fulfillment-sop/references/pitfalls.md +30 -0
- package/engine/skills/ml-full-appointment/SKILL.md +98 -0
- package/engine/version.mjs +1 -1
- package/package.json +1 -1
- package/engine/scripts/drill-clean-machine.sh +0 -315
- package/engine/scripts/install-internal-role.sh +0 -190
- package/engine/scripts/install-one-shot.mjs +0 -665
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +0 -297
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +0 -11
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +0 -39
- package/engine/scripts/patches/verify-bundled-migrators.mjs +0 -79
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +0 -227
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ml-ads.mjs —— Mercado Ads(Product Ads)**只读**效果报告 CLI。
|
|
4
|
+
*
|
|
5
|
+
* ## 干什么
|
|
6
|
+
* 用与日报同一条授权链(`~/.dsh/argent/ml-api-stores.json`)逐店拉广告数据,输出:
|
|
7
|
+
* 花费 / 广告销售额 / ROAS / ACoS / 点击 / CTR / CPC / 广告单量,
|
|
8
|
+
* 外加**告警**(ACoS 超标、烧钱零转化、ROAS 过低)和活动级下钻。
|
|
9
|
+
*
|
|
10
|
+
* ## 用法
|
|
11
|
+
* node scripts/ml-ads.mjs # 全部已授权店铺 · 近 7 个完整自然日
|
|
12
|
+
* node scripts/ml-ads.mjs --store 1店 # 店铺子串过滤
|
|
13
|
+
* node scripts/ml-ads.mjs --days 30 # 回溯天数(MELI 上限 90)
|
|
14
|
+
* node scripts/ml-ads.mjs --to 2026-10-16 # 指定结束日(站点本地)
|
|
15
|
+
* node scripts/ml-ads.mjs --detail # 附「花钱最多 / ACoS 超标」活动
|
|
16
|
+
* node scripts/ml-ads.mjs --json # 结构化输出(stdout 纯 JSON)
|
|
17
|
+
* node scripts/ml-ads.mjs --lines # 只输出中文卡片行(给日报/角色回复用)
|
|
18
|
+
* node scripts/ml-ads.mjs --list # 只列每店广告主,不拉活动
|
|
19
|
+
* node scripts/ml-ads.mjs --acos-bad 30 --min-spend 200 # 告警阈值
|
|
20
|
+
*
|
|
21
|
+
* ## 只读纪律
|
|
22
|
+
* · 只发 GET(`/advertising/...`);⛔ 不改预算/暂停/调价,也不写任何客户数据;
|
|
23
|
+
* · 唯一写盘 = token 轮换回写(MELI refresh_token 是轮换制,不写回下次必 429):
|
|
24
|
+
* 原子写(tmp + rename)、保持 600;
|
|
25
|
+
* · API 基址:`ML_API_BASE` → 默认平台地址(引擎 lib 里⛔不含域名,只在本 CLI 注入)。
|
|
26
|
+
*/
|
|
27
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, chmodSync } from 'node:fs'
|
|
28
|
+
import { join, dirname } from 'node:path'
|
|
29
|
+
import { fileURLToPath } from 'node:url'
|
|
30
|
+
import { resolveDataDir } from './lib/argent-datadir.mjs'
|
|
31
|
+
import {
|
|
32
|
+
loadEnvFile, refreshStoreToken, apiGet as coreApiGet,
|
|
33
|
+
SITE_TZ, SITE_CURRENCY, fallbackSite, startOfLocalDayUTC, localDateStr, matchStores,
|
|
34
|
+
} from './lib/daily-report-core.mjs'
|
|
35
|
+
import {
|
|
36
|
+
fetchAdsReport, classifyAdsAlerts, adsCardLines, fmtMoney, ADS_ALERT_DEFAULTS,
|
|
37
|
+
} from './lib/ml-ads-core.mjs'
|
|
38
|
+
|
|
39
|
+
const API_BASE = () => process.env.ML_API_BASE || 'https://api.mercadolibre.com'
|
|
40
|
+
|
|
41
|
+
const args = process.argv.slice(2)
|
|
42
|
+
const argVal = (n) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] ? args[i + 1] : null }
|
|
43
|
+
const has = (n) => args.includes(n)
|
|
44
|
+
|
|
45
|
+
if (has('-h') || has('--help')) {
|
|
46
|
+
console.log(`Mercado Ads 只读报告
|
|
47
|
+
|
|
48
|
+
用法: node scripts/ml-ads.mjs [选项]
|
|
49
|
+
--store <名或uid> 店铺过滤(名称子串 / uid 全等,与日报同一判据)
|
|
50
|
+
--days <n> 回溯天数,默认 7(MELI 上限 90)
|
|
51
|
+
--to <YYYY-MM-DD> 结束日(站点本地),默认「昨天」
|
|
52
|
+
--detail 附活动级下钻(花钱最多 + ACoS 超标)
|
|
53
|
+
--list 只列出每店广告主(1 请求/店)
|
|
54
|
+
--json 结构化 JSON(stdout)
|
|
55
|
+
--lines 中文卡片行(给日报分区/角色回复复用)
|
|
56
|
+
--acos-warn <n> ACoS 黄色阈值,默认 ${ADS_ALERT_DEFAULTS.acosWarn}
|
|
57
|
+
--acos-bad <n> ACoS 红色阈值,默认 ${ADS_ALERT_DEFAULTS.acosBad}
|
|
58
|
+
--min-spend <n> 告警最小花费(本币),默认 ${ADS_ALERT_DEFAULTS.minSpend}
|
|
59
|
+
--env <path> daily-report.env 路径
|
|
60
|
+
退出码: 0 正常 | 1 用法/取数失败 | 2 全部店铺失败`)
|
|
61
|
+
process.exit(0)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DATA_DIR = resolveDataDir({ env: process.env }).dir
|
|
65
|
+
const ENV_PATH = argVal('--env') || join(DATA_DIR, 'daily-report.env')
|
|
66
|
+
const STORES_PATH = process.env.ML_API_STORES_JSON || join(DATA_DIR, 'ml-api-stores.json')
|
|
67
|
+
const env = loadEnvFile(ENV_PATH)
|
|
68
|
+
const doc = existsSync(STORES_PATH) ? JSON.parse(readFileSync(STORES_PATH, 'utf8')) : { stores: [] }
|
|
69
|
+
const stores = doc.stores || []
|
|
70
|
+
|
|
71
|
+
const persistStores = () => {
|
|
72
|
+
try {
|
|
73
|
+
mkdirSync(dirname(STORES_PATH), { recursive: true })
|
|
74
|
+
const tmp = `${STORES_PATH}.tmp`
|
|
75
|
+
writeFileSync(tmp, JSON.stringify({ stores }, null, 2) + '\n', { mode: 0o600 })
|
|
76
|
+
renameSync(tmp, STORES_PATH)
|
|
77
|
+
chmodSync(STORES_PATH, 0o600)
|
|
78
|
+
} catch (e) { console.error(`✗ token 写回失败:${e.message}`) }
|
|
79
|
+
}
|
|
80
|
+
const apiGet = (path, token) => coreApiGet(path, token, { base: API_BASE() })
|
|
81
|
+
|
|
82
|
+
const alertOpts = {
|
|
83
|
+
acosWarn: Number(argVal('--acos-warn') || ADS_ALERT_DEFAULTS.acosWarn),
|
|
84
|
+
acosBad: Number(argVal('--acos-bad') || ADS_ALERT_DEFAULTS.acosBad),
|
|
85
|
+
minSpend: Number(argVal('--min-spend') || ADS_ALERT_DEFAULTS.minSpend),
|
|
86
|
+
minRoas: ADS_ALERT_DEFAULTS.minRoas,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const FLAG_JSON = has('--json')
|
|
90
|
+
const FLAG_LINES = has('--lines')
|
|
91
|
+
const FLAG_DETAIL = has('--detail')
|
|
92
|
+
const FLAG_LIST = has('--list')
|
|
93
|
+
const days = Math.max(1, Math.min(90, Number(argVal('--days') || 7)))
|
|
94
|
+
const toArg = argVal('--to')
|
|
95
|
+
|
|
96
|
+
const now = new Date()
|
|
97
|
+
const picked = matchStores(stores, (argVal('--store') || '').split(',').map((x) => x.trim()).filter(Boolean))
|
|
98
|
+
if (!picked.length) {
|
|
99
|
+
console.error(`✗ 没有匹配到店铺(已授权 ${stores.length} 家)`)
|
|
100
|
+
process.exit(1)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const results = []
|
|
104
|
+
for (const store of picked) {
|
|
105
|
+
const site = fallbackSite(store.site)
|
|
106
|
+
const tz = SITE_TZ[site]
|
|
107
|
+
const todayStart = startOfLocalDayUTC(tz, now)
|
|
108
|
+
const toMs = toArg ? startOfLocalDayUTC(tz, new Date(`${toArg}T12:00:00Z`)) : todayStart - 86400000
|
|
109
|
+
const fromMs = toMs - (days - 1) * 86400000
|
|
110
|
+
const dateFrom = localDateStr(tz, new Date(fromMs))
|
|
111
|
+
const dateTo = localDateStr(tz, new Date(toMs))
|
|
112
|
+
const row = { store: store.name, site, uid: store.user_id, currency: SITE_CURRENCY[site] || '', dateFrom, dateTo }
|
|
113
|
+
try {
|
|
114
|
+
const token = await refreshStoreToken(env, store, { onRotate: persistStores })
|
|
115
|
+
if (FLAG_LIST) {
|
|
116
|
+
const adv = await apiGet('/advertising/advertisers?product_id=PADS', token)
|
|
117
|
+
row.advertisers = (adv.advertisers || []).map((a) => ({ advertiser_id: a.advertiser_id, site: a.site_id, name: a.advertiser_name }))
|
|
118
|
+
results.push(row)
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const report = await fetchAdsReport({ apiGet, token, site, dateFrom, dateTo })
|
|
122
|
+
row.report = report
|
|
123
|
+
row.alerts = classifyAdsAlerts(report.totals, alertOpts)
|
|
124
|
+
results.push(row)
|
|
125
|
+
} catch (e) {
|
|
126
|
+
row.error = String(e.message || e)
|
|
127
|
+
results.push(row)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── 输出 ───────────────────────────────────────────────────
|
|
132
|
+
const ok = results.filter((r) => !r.error)
|
|
133
|
+
if (!ok.length) {
|
|
134
|
+
if (FLAG_JSON) console.log(JSON.stringify({ results }, null, 2))
|
|
135
|
+
else for (const r of results) console.error(`✗ ${r.store}:${r.error}`)
|
|
136
|
+
process.exit(2)
|
|
137
|
+
}
|
|
138
|
+
if (FLAG_JSON) { console.log(JSON.stringify({ generatedAt: now.toISOString(), days, results }, null, 2)); process.exit(0) }
|
|
139
|
+
|
|
140
|
+
if (FLAG_LINES) {
|
|
141
|
+
for (const r of ok) {
|
|
142
|
+
const lines = adsCardLines(r.report, { currency: r.currency, alertOpts })
|
|
143
|
+
console.log(`### ${r.store}`)
|
|
144
|
+
console.log(lines.join('\n'))
|
|
145
|
+
}
|
|
146
|
+
process.exit(0)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (FLAG_LIST) {
|
|
150
|
+
for (const r of results) {
|
|
151
|
+
if (r.error) { console.log(`${r.store}: ✗ ${r.error}`); continue }
|
|
152
|
+
console.log(`${r.store}(${r.site}):` + (r.advertisers.length ? r.advertisers.map((a) => `${a.advertiser_id}(${a.site})`).join(', ') : '无广告主'))
|
|
153
|
+
}
|
|
154
|
+
process.exit(0)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const hdr = ['店铺', '站点', '花费', '销售额', 'ROAS', 'ACoS', '点击', 'CTR', '广告单量', '活动']
|
|
158
|
+
let text = `## 📣 Mercado Ads · ${ok[0].dateFrom} ~ ${ok[0].dateTo}(近 ${days} 天)\n\n`
|
|
159
|
+
text += '| ' + hdr.join(' | ') + ' |\n|' + hdr.map(() => '---').join('|') + '|\n'
|
|
160
|
+
const allAlerts = []
|
|
161
|
+
for (const r of results) {
|
|
162
|
+
if (r.error) { text += `| ${r.store} | ${r.site} | — | — | — | — | — | — | — | ✗ ${r.error.slice(0, 40)} |\n`; continue }
|
|
163
|
+
const t = r.report.totals
|
|
164
|
+
const c = r.currency ? r.currency + ' ' : ''
|
|
165
|
+
text += `| ${r.store} | ${r.site} | ${c}${fmtMoney(t.cost)} | ${c}${fmtMoney(t.amount)} | ${t.roas} | ${t.acos}% | ${t.clicks} | ${t.ctr}% | ${t.units} | ${t.campaigns} |\n`
|
|
166
|
+
for (const a of r.alerts) allAlerts.push({ store: r.store, ...a })
|
|
167
|
+
if (FLAG_DETAIL) {
|
|
168
|
+
for (const a of r.report.advertisers) {
|
|
169
|
+
const top = [...(a.topCampaigns.top || [])]
|
|
170
|
+
for (const cp of top) text += `| ↳ ${r.store}·${cp.name.slice(0, 24)} | ${a.advertiserId} | ${c}${fmtMoney(cp.cost)} | ${c}${fmtMoney(cp.amount)} | ${cp.roas} | ${cp.acos}% | ${cp.clicks} | — | ${cp.units} | — |\n`
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
text += '\n'
|
|
175
|
+
if (allAlerts.length) {
|
|
176
|
+
text += '### ⚠️ 告警\n'
|
|
177
|
+
for (const a of allAlerts) text += `- ${a.level === 'red' ? '🔴' : '🟡'} **${a.store}**:${a.text}\n`
|
|
178
|
+
} else {
|
|
179
|
+
text += '✅ 无告警(ACoS 与 ROAS 均在阈值内)\n'
|
|
180
|
+
}
|
|
181
|
+
console.log(text)
|
|
@@ -49,7 +49,7 @@ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
|
49
49
|
// 其他站点域名实测被拦(403),后续逐个站点验证后更新此表。
|
|
50
50
|
const AUTH_HOSTS = {
|
|
51
51
|
MLM: 'auth.mercadolibre.com.mx',
|
|
52
|
-
MLB: 'auth.
|
|
52
|
+
MLB: 'auth.mercadolivre.com.br', // 2026-10-17 实测修正:巴西站点域名是 mercadolivre(auth.mercadolibre.com.br 无 DNS)
|
|
53
53
|
MLA: 'auth.mercadolibre.com.ar',
|
|
54
54
|
MLC: 'auth.mercadolibre.cl',
|
|
55
55
|
MCO: 'auth.mercadolibre.com.co',
|
|
@@ -7349,6 +7349,16 @@ class Orchestrator:
|
|
|
7349
7349
|
i = j + 1
|
|
7350
7350
|
return ",".join(out)
|
|
7351
7351
|
|
|
7352
|
+
def _write_status_done(self, record_id: str) -> None:
|
|
7353
|
+
"""**唯一入口**:把 `FULL状态` 置为「已完成」。
|
|
7354
|
+
|
|
7355
|
+
2026-10-16:原先只有 `_finalize_ticket_complete`(引擎自己跑完)能置位;现在
|
|
7356
|
+
「运营手动完成(hub-v2 实测)」也要置位(用户口径「都要」)⇒ 抽出本入口,两条路径
|
|
7357
|
+
**都走这里**,保持"表状态只有一个写点"的不变量(回归见 ticket-completion-criteria /
|
|
7358
|
+
step-progress-realtime 的 AST 断言)。
|
|
7359
|
+
"""
|
|
7360
|
+
self.feishu.update_status(record_id, "done")
|
|
7361
|
+
|
|
7352
7362
|
def _finalize_ticket_complete(self, *, label: str = "全部完成",
|
|
7353
7363
|
notify: bool = True) -> bool:
|
|
7354
7364
|
"""**唯一**的「整票完成态」写入入口(`FULL状态=已完成` + `当前步骤` + 完成卡)。
|
|
@@ -7372,7 +7382,7 @@ class Orchestrator:
|
|
|
7372
7382
|
"field": "ticket-complete", "value": label[:120],
|
|
7373
7383
|
"why": "premature-completion-ticket", "missing_steps": missing})
|
|
7374
7384
|
return False
|
|
7375
|
-
self.
|
|
7385
|
+
self._write_status_done(self.state.record_id)
|
|
7376
7386
|
self.feishu.update_step_progress(self.state.record_id, label, force=True, done=True,
|
|
7377
7387
|
ticket_attestation=self._ticket_attestation())
|
|
7378
7388
|
self.feishu.set_confirm(self.state.record_id, False)
|
|
@@ -7761,11 +7771,11 @@ class Orchestrator:
|
|
|
7761
7771
|
## 写什么 / ⛔ 不写什么(用户口径)
|
|
7762
7772
|
- **写**:`FULL备注` ← 「识别为运营手动完成(依据 hub-v2 实测):<逐项>」(同源同文,
|
|
7763
7773
|
与 `--mode hub-status` 打印、与 `failures.jsonl#hub_status` 完全一致);
|
|
7764
|
-
`当前步骤` ←
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7774
|
+
`当前步骤` ← **「全部完成」**(2026-10-16 用户口径:表口径与"引擎自己跑完"一致);`就绪=false`(消费掉拾取位)。
|
|
7775
|
+
**`FULL状态` ← 已完成**(2026-10-16 用户口径「都要」:认定即置位,否则票一直挂在"失败"里,
|
|
7776
|
+
运营每轮巡检都要人工排除;判据仍是 hub-v2 三项全 True,比表里的旧状态权威)。
|
|
7777
|
+
- **仍不写**:不发「FULL 货件完成」卡、不写 `ticket_attestation`(卡与存证只属于引擎自己
|
|
7778
|
+
跑完的路径);"谁完成的"这件事仍由 `FULL备注` 那句话交代。
|
|
7769
7779
|
|
|
7770
7780
|
返回 `{"status": "already_completed", "identified_as": "manual-completion-by-hub", …}`
|
|
7771
7781
|
⇒ `_execute_steps` 见 `already_completed` 即停止后续步骤。
|
|
@@ -7773,13 +7783,15 @@ class Orchestrator:
|
|
|
7773
7783
|
rid = self.state.record_id
|
|
7774
7784
|
note = f"{self.HUB_MANUAL_NOTE}:" + ";".join(verdict.get("items") or [])
|
|
7775
7785
|
self._log(f" 🧾 {self.HUB_MANUAL_NOTE}:{verdict.get('reason') or ''}"
|
|
7776
|
-
f"(货件 #{self.state.shipment_id})——
|
|
7786
|
+
f"(货件 #{self.state.shipment_id})—— 并置 FULL状态=已完成(2026-10-16 用户口径)")
|
|
7777
7787
|
if rid and not getattr(self.feishu, "dry_run", False):
|
|
7778
7788
|
try:
|
|
7779
7789
|
self.feishu.set_note(rid, note[:900])
|
|
7780
|
-
self.feishu.update_step_progress(rid, "
|
|
7790
|
+
self.feishu.update_step_progress(rid, "全部完成",
|
|
7781
7791
|
force=True)
|
|
7782
7792
|
self.feishu.set_confirm(rid, False)
|
|
7793
|
+
# 2026-10-16 用户口径(「都要」):认定运营手动完成 ⇒ 一并置 FULL状态=已完成
|
|
7794
|
+
self._write_status_done(rid)
|
|
7783
7795
|
except Exception as exc: # noqa: BLE001
|
|
7784
7796
|
self._log(f" ⚠️ 写「运营手动完成」备注失败(不影响判定): {exc}")
|
|
7785
7797
|
return {
|
|
@@ -7788,10 +7800,29 @@ class Orchestrator:
|
|
|
7788
7800
|
"identified_as": "manual-completion-by-hub",
|
|
7789
7801
|
"hub_note": note[:900],
|
|
7790
7802
|
"hub_status": verdict,
|
|
7791
|
-
"status_unchanged":
|
|
7803
|
+
"status_unchanged": False, # 已改写 FULL状态(2026-10-16 起)
|
|
7804
|
+
"status_written": "已完成", # 与 hub 判据同源;下游可据此确认表已置位
|
|
7792
7805
|
}
|
|
7793
7806
|
|
|
7794
7807
|
# ---- 本地失败诊断(failures.jsonl 索引)----
|
|
7808
|
+
async def _hub_manual_step7_fallback(self) -> bool:
|
|
7809
|
+
"""步骤7 收口前先问 hub 真相:门户全完成 ⇒ 认定「运营手动完成」并返回 True。
|
|
7810
|
+
|
|
7811
|
+
KB-137(2026-10-16 现场 79301358):单步/异常路径会绕过前置「先问 hub 真相」判定,
|
|
7812
|
+
于是"门户其实早已全完成(运营手工做完)"的票被判失败留在表里。补同一判据
|
|
7813
|
+
(唯一入口 `_hub_truth` + `_hub_verdict_of`),与前置检查同源同文;探针不可用 ⇒ False。
|
|
7814
|
+
"""
|
|
7815
|
+
try:
|
|
7816
|
+
hub = await self._hub_truth()
|
|
7817
|
+
verdict = self._hub_verdict_of(hub)
|
|
7818
|
+
except Exception as exc: # noqa: BLE001(探针失败 ⇒ 沿用原判定)
|
|
7819
|
+
self._log(f" ⚠️ 步骤7 收口前的 hub 探针不可用(沿用原判定): {exc}")
|
|
7820
|
+
return False
|
|
7821
|
+
if verdict.get("operationally_complete"):
|
|
7822
|
+
self._hub_manual_complete(verdict)
|
|
7823
|
+
return True
|
|
7824
|
+
return False
|
|
7825
|
+
|
|
7795
7826
|
def _current_log_file(self) -> str:
|
|
7796
7827
|
"""本次运行日志路径:优先 poll 脚本注入的 FULFILLMENT_LOG_FILE,兜底按店铺名推导。"""
|
|
7797
7828
|
env_path = os.environ.get("FULFILLMENT_LOG_FILE", "").strip()
|
|
@@ -11019,6 +11050,20 @@ class Orchestrator:
|
|
|
11019
11050
|
except Exception:
|
|
11020
11051
|
return False
|
|
11021
11052
|
|
|
11053
|
+
async def _declara_progress(self) -> tuple[int, int]:
|
|
11054
|
+
"""返回 (已勾选 checkbox, 总数) —— 区分「残留文案」与「声明确实没做完」。
|
|
11055
|
+
|
|
11056
|
+
KB-136(2026-10-16 现场 79301358):4 个箱子只声明了 2 个 ⇒ 「Descarga todas」永远不会
|
|
11057
|
+
出现,而旧逻辑把点击前就存在的「Declara al menos」一律当**残留** ⇒ 干等 30s 后以
|
|
11058
|
+
"平台侧异常"收口(冤枉平台、运营也拿不到可执行信息)。读不到 ⇒ (-1,-1),不改变原判定。
|
|
11059
|
+
"""
|
|
11060
|
+
try:
|
|
11061
|
+
raw = await self._generar_diag()
|
|
11062
|
+
d = json.loads(raw) if raw else {}
|
|
11063
|
+
return int(d.get("checkbox_checked") or 0), int(d.get("checkbox_total") or 0)
|
|
11064
|
+
except Exception: # noqa: BLE001(读不到 ⇒ 沿用原判定)
|
|
11065
|
+
return -1, -1
|
|
11066
|
+
|
|
11022
11067
|
async def _generar_diag(self) -> str:
|
|
11023
11068
|
"""KB-128:采集 Generar/Descarga 按钮态 + checkbox 勾选态 + Declara 提示 + URL。
|
|
11024
11069
|
|
|
@@ -11233,6 +11278,33 @@ class Orchestrator:
|
|
|
11233
11278
|
# ② 提示在**点击 Generar 之前就已存在** = 残留(页面流转留下的旧校验文案),
|
|
11234
11279
|
# 不能当作"checkbox 未生效"的证据 ⇒ 不作为失败信号,继续等。
|
|
11235
11280
|
if self._declara_hint_at_click:
|
|
11281
|
+
# KB-136:先看勾选进度 —— 不是残留而是**声明确实没做完**时,干等必然超时。
|
|
11282
|
+
_declared, _total = await self._declara_progress()
|
|
11283
|
+
_andes_ok = await self._andes_checked()
|
|
11284
|
+
# ⚠️ checkbox_total 含我们**故意跳过**的 #1(bultos)⇒ "勾选数 < 总数"本身不是声明
|
|
11285
|
+
# 未完成的证据(正常态就是 2/4)。必须再看 Andes 勾选态(#2/#3 是否到位)。
|
|
11286
|
+
if _total > 0 and _declared < _total and not _andes_ok:
|
|
11287
|
+
if retried >= 1:
|
|
11288
|
+
raise StepError(
|
|
11289
|
+
7, "business",
|
|
11290
|
+
f"[declaration_incomplete] 箱型/箱数声明只完成 {_declared}/{_total}"
|
|
11291
|
+
f"(「Declara al menos」仍在)⇒ 下载按钮不会出现;需在该页面把剩下的箱子也声明完后重跑。"
|
|
11292
|
+
f"现场: {await self._generar_diag()}",
|
|
11293
|
+
recovery_attempted=["declara_progress_probe", "andes_checkbox_recheck"])
|
|
11294
|
+
retried += 1
|
|
11295
|
+
self._log(f" ⚠️ 「Declara al menos」为**真实未完成**(已声明 {_declared}/{_total})"
|
|
11296
|
+
f"⇒ 重勾 Andes checkbox 并重新触发 Generar(第 {retried}/2 次)")
|
|
11297
|
+
try:
|
|
11298
|
+
shot = await self._screenshot_on_error(7)
|
|
11299
|
+
if shot:
|
|
11300
|
+
self._log(f" 📸 重试现场截图(供识图判读): {shot}")
|
|
11301
|
+
except Exception:
|
|
11302
|
+
pass
|
|
11303
|
+
await self._check_andes_checkboxes()
|
|
11304
|
+
self._declara_hint_at_click = await self._has_declara_hint()
|
|
11305
|
+
await self.browser.click_with_fallback(7, "generate_btn", "Generar etiquetas", wait_after=5.0)
|
|
11306
|
+
waited = 0.0
|
|
11307
|
+
continue
|
|
11236
11308
|
if not stale_logged:
|
|
11237
11309
|
self._log(" ℹ️ 「Declara al menos」在点击 Generar **之前**就已存在"
|
|
11238
11310
|
"(残留文案)⇒ 不作为失败信号,继续等待生成结果")
|
|
@@ -12277,7 +12349,16 @@ class Orchestrator:
|
|
|
12277
12349
|
elif step == 6:
|
|
12278
12350
|
await self.step6_labels(dry_run=dry_run)
|
|
12279
12351
|
elif step == 7:
|
|
12280
|
-
|
|
12352
|
+
try:
|
|
12353
|
+
await self.step7_box_labels()
|
|
12354
|
+
except StepError:
|
|
12355
|
+
# KB-137:收口前先问 hub 真相(门户可能早已完成,只是页面态残留)
|
|
12356
|
+
if await self._hub_manual_step7_fallback():
|
|
12357
|
+
return self._result("already_completed", None,
|
|
12358
|
+
{"step": 7, "type": "hub_manual_complete",
|
|
12359
|
+
"message": self.HUB_MANUAL_NOTE},
|
|
12360
|
+
step_summaries, dry_run)
|
|
12361
|
+
raise
|
|
12281
12362
|
elif step == 8:
|
|
12282
12363
|
await self.step8_cancel_appointment()
|
|
12283
12364
|
# ── 需求 2:步骤**结束**(成功)再写一次(节流;文案与开跑不同 ⇒ 必写) ──
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* P2 执行面回写器(本机 → 服务端)—— 用 runner token 把一次执行结果写回数据中心。
|
|
4
|
+
*
|
|
5
|
+
* 只做三件事(都不含业务判定):
|
|
6
|
+
* 1) --create-card <title> 建服务端业务卡(可带 --preset)
|
|
7
|
+
* 2) --report 写 run(POST /runs)+ 结算卡状态(PATCH /cards/:id)
|
|
8
|
+
* 3) --verify 读回卡与 run,供自动核对
|
|
9
|
+
*
|
|
10
|
+
* 用法:
|
|
11
|
+
* node scripts/p2-report-run.mjs --state /tmp/argent-dev-local.json --create-card "美客多日报(试点)" --preset argent-ml-daily
|
|
12
|
+
* node scripts/p2-report-run.mjs --state ... --report --card-id <id> --preset argent-ml-daily --outcome succeeded --tokens 120 --summary "链路验证通过"
|
|
13
|
+
* node scripts/p2-report-run.mjs --state ... --verify --card-id <id>
|
|
14
|
+
*
|
|
15
|
+
* 状态文件(600)由 `whyshu-svr/scripts/dev-local.mjs` 生成:{ serverUrl, runnerToken, adminToken, appId }
|
|
16
|
+
* ⛔ 不打印 token;⛔ 不写生产(state 文件只由本地 dev-local 生成)。
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
const argv = process.argv.slice(2);
|
|
21
|
+
const val = (flag, dflt = '') => {
|
|
22
|
+
const i = argv.indexOf(flag);
|
|
23
|
+
return i > -1 ? (argv[i + 1] ?? dflt) : dflt;
|
|
24
|
+
};
|
|
25
|
+
const has = (flag) => argv.includes(flag);
|
|
26
|
+
|
|
27
|
+
const statePath = val('--state', '/tmp/argent-dev-local.json');
|
|
28
|
+
let state;
|
|
29
|
+
try { state = JSON.parse(readFileSync(statePath, 'utf8')); } catch (e) {
|
|
30
|
+
console.error(`❌ 读不到状态文件 ${statePath}:${e.message}\n 先在 whyshu-svr 起本地服务:node scripts/dev-local.mjs`);
|
|
31
|
+
process.exit(2);
|
|
32
|
+
}
|
|
33
|
+
const { serverUrl, runnerToken, appId } = state;
|
|
34
|
+
if (!serverUrl || !runnerToken || !appId) { console.error('❌ 状态文件缺 serverUrl/runnerToken/appId'); process.exit(2); }
|
|
35
|
+
|
|
36
|
+
const api = async (path, { method = 'GET', body } = {}) => {
|
|
37
|
+
const res = await fetch(`${serverUrl}/api/argent/v1${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: {
|
|
40
|
+
authorization: `Bearer ${runnerToken}`,
|
|
41
|
+
'content-type': 'application/json',
|
|
42
|
+
...(has('--idem-key') ? { 'idempotency-key': val('--idem-key') } : {}),
|
|
43
|
+
},
|
|
44
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
45
|
+
});
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
let data = null;
|
|
48
|
+
try { data = JSON.parse(text); } catch { data = text; }
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(`${method} ${path} → ${res.status} ${data?.error?.code || ''} ${data?.error?.message || ''}`.trim());
|
|
51
|
+
}
|
|
52
|
+
return data;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const main = async () => {
|
|
56
|
+
const tenantPath = `/tenants/${appId}`;
|
|
57
|
+
|
|
58
|
+
if (has('--create-card')) {
|
|
59
|
+
const title = val('--create-card');
|
|
60
|
+
const preset = val('--preset');
|
|
61
|
+
const d = await api(`${tenantPath}/cards`, { method: 'POST', body: { title, description: val('--description'), assigneePreset: preset } });
|
|
62
|
+
console.log(JSON.stringify({ ok: true, cardId: d.data.card._id, title, appId }));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (has('--report')) {
|
|
67
|
+
const cardId = val('--card-id');
|
|
68
|
+
const preset = val('--preset', 'argent-ml-daily');
|
|
69
|
+
const outcome = val('--outcome', 'succeeded');
|
|
70
|
+
const tokens = Number(val('--tokens', '0')) || 0;
|
|
71
|
+
const summary = val('--summary', '');
|
|
72
|
+
const run = await api(`${tenantPath}/runs`, { method: 'POST', body: { cardId: cardId || null, preset } });
|
|
73
|
+
const runId = run.data.run._id;
|
|
74
|
+
const settled = await api(`${tenantPath}/runs/${runId}/settle`, {
|
|
75
|
+
method: 'PATCH',
|
|
76
|
+
body: { outcome, tokens, cost: Number(val('--cost', '0')) || 0, summary },
|
|
77
|
+
});
|
|
78
|
+
console.log(JSON.stringify({
|
|
79
|
+
ok: true,
|
|
80
|
+
runId,
|
|
81
|
+
outcome: settled.data.run.outcome,
|
|
82
|
+
cardStatus: settled.data.card?.status ?? null,
|
|
83
|
+
cardSkipped: settled.data.cardSkipped ?? false,
|
|
84
|
+
}));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (has('--verify')) {
|
|
89
|
+
const cardId = val('--card-id');
|
|
90
|
+
const cards = await api(`${tenantPath}/cards?limit=50`);
|
|
91
|
+
const runs = await api(`${tenantPath}/runs?limit=50`);
|
|
92
|
+
const card = cardId ? cards.data.items.find((c) => String(c._id) === String(cardId)) : null;
|
|
93
|
+
const relatedRuns = cardId ? runs.data.items.filter((r) => String(r.cardId) === String(cardId)) : [];
|
|
94
|
+
console.log(JSON.stringify({ ok: true, card, runs: relatedRuns }, null, 2));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.error('用法:--create-card <title> | --report ... | --verify ...(详见脚本头注释)');
|
|
99
|
+
process.exit(2);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
main().catch((e) => { console.error(`❌ ${e.message}`); process.exit(1); });
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
+
# 加固(B 类 · 防毒化):Session.append 边界补 message 形状校验 —— **dsh 0.2.1-rc.2 移植版**
|
|
2
|
+
#
|
|
3
|
+
# 背景(2026-09-18 会话毒化事故):append() 原本只做 validateSessionEventData + surface 校验,
|
|
4
|
+
# **漏了** assertMessageEventShape ⇒ 一个非法 message 事件(如某个 tool 的 render 返回裸字符串)
|
|
5
|
+
# 能写进日志,直到**下次从磁盘加载**才炸("message must contain one tool-result block"),
|
|
6
|
+
# 此时会话根本打不开,surface replace 也救不回来(校验针对事件本身,不是 surface 视图)。
|
|
7
|
+
#
|
|
8
|
+
# 版本沿革 / 为什么这次要改:
|
|
9
|
+
# · 2026-09-18 首版:面向 0.1.7/0.2.0 —— 插入点是 `validateSessionEventData(...)` 与
|
|
10
|
+
# `this.surfaceManager.validateNext(event)` 之间(当时 append() 内联了这个调用)。
|
|
11
|
+
# · 2026-10-18 移植到 **0.2.1-rc.2**:上游把 `surfaceManager.validateNext(event)` 移进了
|
|
12
|
+
# `commit()`,append() 变成 `validateSessionEventData(...) → this.commit(event, entry)`;
|
|
13
|
+
# 旧 hunk 的上下文消失 ⇒ 补丁应用失败、加固门禁变红(实测)。故插入点改为
|
|
14
|
+
# **`validateSessionEventData(...)` 之后、`this.commit(event, entry)` 之前**。
|
|
15
|
+
# · ⚠️ 上游 0.2.1 **并未**原生补上该校验:`assertMessageEventShape` 只在 `adoptSessionEvent()`
|
|
16
|
+
# 里调用(那是另一条"接收获批事件"的路径),append/commit 仍未调用 ⇒ 加固仍然必要。
|
|
17
|
+
# · 0.1.7/0.2.0 树的旧版补丁见 git 历史(本文件在提交 987de39 之前的版本);回滚到旧树时
|
|
18
|
+
# 物理备份树里**已含**该补丁的修改,无需重打;`--patches-only` 在旧树上会对此文件报 warn,
|
|
19
|
+
# 属预期(按 `dsh-upgrade.mjs --rollback` 流程走,别拿"先别重启"卡自己)。
|
|
20
|
+
#
|
|
21
|
+
# 退役条件:上游把 assertMessageEventShape 接进 append/commit 路径后,patch --forward 会因上下文不符
|
|
22
|
+
# 而 warn ⇒ 届时删除本文件并同步删掉 scripts/patches/verify-hardening.mjs 的补丁 B 部分。
|
|
1
23
|
--- /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session/lib/index.js.orig
|
|
2
24
|
+++ /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session/lib/index.js
|
|
3
|
-
@@ -
|
|
25
|
+
@@ -1533,6 +1533,15 @@
|
|
4
26
|
...surfaceMetadataSnapshot
|
|
5
27
|
});
|
|
6
28
|
validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
|
|
7
|
-
+ /* Hardening (2026-09-18, argent-dsh hotfix
|
|
8
|
-
+ message event that could never be replayed.
|
|
9
|
-
+ (e.g. a render that returned a bare string)
|
|
10
|
-
+ when the session is next LOADED from disk — by
|
|
11
|
-
+ opened, and a surface replace cannot repair it.
|
|
29
|
+
+ /* Hardening (2026-09-18, argent-dsh hotfix;2026-10-18 移植到 dsh 0.2.1):
|
|
30
|
+
+ the append boundary must reject a message event that could never be replayed.
|
|
31
|
+
+ Otherwise a malformed tool/result (e.g. a render that returned a bare string)
|
|
32
|
+
+ lands in the log and only fails when the session is next LOADED from disk — by
|
|
33
|
+
+ then the session cannot even be opened, and a surface replace cannot repair it.
|
|
34
|
+
+ Fail at the writer instead.
|
|
35
|
+
+ 0.2.1 结构变化:append() 不再直接调 surfaceManager.validateNext(移进 commit()),
|
|
36
|
+
+ 故插入点改为 validateSessionEventData 之后、commit 之前;校验函数/语义未变。 */
|
|
12
37
|
+ assertMessageEventShape(event, `session event "${type}" at seq ${event.seq}`);
|
|
13
|
-
this.
|
|
14
|
-
|
|
15
|
-
|
|
38
|
+
this.commit(event, entry);
|
|
39
|
+
return event;
|
|
40
|
+
}
|
|
@@ -1,20 +1,44 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 放宽 v4 加载校验:允许"turn 结束后的**替换型** tool/result"(合法历史,不是损坏)
|
|
2
|
+
#
|
|
3
|
+
# 背景(2026-10-17,本机 0.1.7-rc.2 → 0.2.0-rc.2 重启后):
|
|
4
|
+
# 用户主会话 06732069(v4、95032 事件)在 0.2.0 上打不开:
|
|
5
|
+
# Failed to load history: stored session "…06732069…" is corrupt:
|
|
6
|
+
# SessionFormatError: tool/result is outside an open turn
|
|
7
|
+
# 真因:v4 校验器(本包 `tool()`)对 `surfaceOp !== "append"` 的 tool/result 一律 `requireTurn`;
|
|
8
|
+
# 而 v3 时代就允许"turn 结束后重写早先 turn 的工具结果"(编辑/重放/surface 自替换)。
|
|
9
|
+
# 本机实测只有 **1 处**:seq 30732,surfaceOp.replace(startSeq=30702,endSeq=30702),
|
|
10
|
+
# data.turn=541 而当时 nextTurn=545 ⇒ 指向**已关闭的历史 turn**,属合法历史。
|
|
11
|
+
#
|
|
12
|
+
# 口径:turn 外时只要求 data.turn 是**已关闭的历史 turn**(安全整数且 < nextTurn);
|
|
13
|
+
# 其余情况仍抛原错 ⇒ 防真损坏,放行合法历史。
|
|
14
|
+
#
|
|
15
|
+
# 来源:等价于已退役 A 类补丁 `dsh-session-format-v3-to-v4--turn-outside-replace.patch`
|
|
16
|
+
# (2026-09-29 为 0.1.7 修同一坑;A 类于 2026-10-16 退役)。0.2.0 是**新旁路树**,
|
|
17
|
+
# 退役补丁不会随树迁移 ⇒ 该缺陷在新树上复现;本文件是 0.2.0 树的移植版,`--patches-only` 会自动重打。
|
|
18
|
+
#
|
|
19
|
+
# 为什么不写 `# requires-dsh:` 版本门:① 本补丁的上下文在 0.1.7 与 0.2.0 上一致,两版都适用;
|
|
20
|
+
# ② `applyHotfixPatches()` 里的版本门目前**静默失效**(正则被行尾注释打断)且依赖未定义的
|
|
21
|
+
# `cmpVerText` —— 写一行能被识别的门反而会 ReferenceError 崩掉升级脚本(待单独修)。
|
|
22
|
+
# 退役条件:上游把这条规则放宽(或提供配置)后,本补丁 `patch --forward` 会因上下文不符而 warn ⇒ 再删。
|
|
2
23
|
--- /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-format-v3-to-v4/lib/index.js.orig
|
|
3
24
|
+++ /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-format-v3-to-v4/lib/index.js
|
|
4
|
-
@@ -617,
|
|
25
|
+
@@ -617,6 +617,19 @@
|
|
5
26
|
}
|
|
6
27
|
tool(event, data) {
|
|
7
28
|
if (event.type === "tool/result" && event["surfaceOp"] !== "append") {
|
|
8
|
-
|
|
9
|
-
+ /* Hardening (2026-09-29, argent-dsh hotfix for 0.1.7): v3 允许"turn 结束后的替换型
|
|
29
|
+
+ /* Argent hotfix(0.2.0-rc.2 移植版,2026-10-17):v3/v4 允许"turn 结束后的替换型
|
|
10
30
|
+ tool/result"(编辑/重放场景:重写早先 turn 的工具结果,如 surface 自替换)。
|
|
11
|
-
+ 原 requireTurn 过严
|
|
12
|
-
+ open turn'
|
|
13
|
-
+
|
|
31
|
+
+ 原 `requireTurn` 过严 ⇒ 整个 session **加载**失败('tool/result is outside an
|
|
32
|
+
+ open turn';本机 06732069 实测 1 处:seq 30732,surfaceOp.replace 指向 seq 30702,
|
|
33
|
+
+ data.turn=541 而当时 nextTurn=545)。放宽口径:turn 外时 data.turn 必须是
|
|
34
|
+
+ **已关闭的历史 turn**(安全整数且 < nextTurn)—— 防真损坏,放行合法历史。
|
|
35
|
+
+ 等价于已退役 A 类补丁 dsh-session-format-v3-to-v4--turn-outside-replace
|
|
36
|
+
+ (0.1.7 上曾修同一坑);本次因 0.2.0 树是**新树**(补丁未随树迁移)而复现。 */
|
|
14
37
|
+ if (this.turn === null) {
|
|
15
38
|
+ const t = data["turn"];
|
|
16
39
|
+ if (!Number.isSafeInteger(t) || t >= this.nextTurn) throw new SessionFormatError(`${event.type} is outside an open turn`);
|
|
40
|
+
+ return;
|
|
17
41
|
+ }
|
|
42
|
+
this.requireTurn(event.type);
|
|
18
43
|
return;
|
|
19
44
|
}
|
|
20
|
-
this.requireStep(event, data);
|
|
@@ -10,10 +10,6 @@ declare -A PKG=(
|
|
|
10
10
|
[dsh-session--append-message-shape]="@deepseek-ai/dsh-session"
|
|
11
11
|
[dsh-session--tool-result-block-017]="@deepseek-ai/dsh-session"
|
|
12
12
|
[dsh-tools--render-return-shape]="@deepseek-ai/dsh-tools"
|
|
13
|
-
[dsh-session-format-v0-to-v1--permission-preset-origin]="@deepseek-ai/dsh-session-format-v0-to-v1"
|
|
14
|
-
[dsh-session-format-v3-to-v4--turn-outside-replace]="@deepseek-ai/dsh-session-format-v3-to-v4"
|
|
15
|
-
[dsh-session-persistence-jsonl--worker-bundled-migrators]="@deepseek-ai/dsh-session-persistence-jsonl"
|
|
16
|
-
[dsh-llm-deepseek--transport-diagnostics]="@deepseek-ai/dsh-llm-deepseek"
|
|
17
13
|
)
|
|
18
14
|
echo "上游版本 $VER | 临时目录 $TMP"
|
|
19
15
|
cd "$TMP"
|
|
@@ -80,7 +80,10 @@ console.log('\n【补丁 B】dsh-session:append 边界补上 message 形状校
|
|
|
80
80
|
if (!existsSync(SESSION)) { ok(false, `dsh-session 存在(${SESSION})`) }
|
|
81
81
|
else {
|
|
82
82
|
const src = readFileSync(SESSION, 'utf8')
|
|
83
|
-
|
|
83
|
+
// 2026-10-18:标记判据放宽为"前缀匹配"——补丁按宿主版本移植时注释尾部会变
|
|
84
|
+
// (0.2.1 版 = `Hardening (2026-09-18, argent-dsh hotfix;2026-10-18 移植到 dsh 0.2.1)`),
|
|
85
|
+
// 但"这是本仓库的补丁、不是上游原生行为"这件事不变。⛔ 不要退回成要求精确闭合括号。
|
|
86
|
+
ok(/Hardening \(2026-09-18, argent-dsh hotfix/.test(src), '打补丁标记存在')
|
|
84
87
|
const appendBody = src.slice(src.indexOf('\tappend(type, data, ...opts) {'), src.indexOf('headerFold;', src.indexOf('\tappend(type, data, ...opts) {')))
|
|
85
88
|
ok(/validateSessionEventData\(event, `session event "\$\{type\}" at seq \$\{event\.seq\}`\);\s*\n\s*\/\* Hardening[\s\S]*?\*\/\s*\n\s*assertMessageEventShape\(event, `session event "\$\{type\}" at seq \$\{event\.seq\}`\);/.test(appendBody),
|
|
86
89
|
'append() 在落盘前调用 assertMessageEventShape()')
|