@whyshu/argent-engine 0.7.3 → 0.7.5
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 +39 -28
- 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 +33 -39
- 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/dsh-upgrade.mjs +18 -5
- 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/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/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 +44 -0
- 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-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-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
|
@@ -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',
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 ⇒ 再删。
|
|
23
|
+
--- /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-format-v3-to-v4/lib/index.js.orig
|
|
24
|
+
+++ /opt/homebrew/lib/node_modules/@deepseek-ai/dsh-session-format-v3-to-v4/lib/index.js
|
|
25
|
+
@@ -617,6 +617,19 @@
|
|
26
|
+
}
|
|
27
|
+
tool(event, data) {
|
|
28
|
+
if (event.type === "tool/result" && event["surfaceOp"] !== "append") {
|
|
29
|
+
+ /* Argent hotfix(0.2.0-rc.2 移植版,2026-10-17):v3/v4 允许"turn 结束后的替换型
|
|
30
|
+
+ tool/result"(编辑/重放场景:重写早先 turn 的工具结果,如 surface 自替换)。
|
|
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 树是**新树**(补丁未随树迁移)而复现。 */
|
|
37
|
+
+ if (this.turn === null) {
|
|
38
|
+
+ const t = data["turn"];
|
|
39
|
+
+ if (!Number.isSafeInteger(t) || t >= this.nextTurn) throw new SessionFormatError(`${event.type} is outside an open turn`);
|
|
40
|
+
+ return;
|
|
41
|
+
+ }
|
|
42
|
+
this.requireTurn(event.type);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
@@ -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()')
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-turn-outside-replace —— 补丁 `dsh-session-format-v3-to-v4--turn-outside-replace` 的自检。
|
|
4
|
+
*
|
|
5
|
+
* 这个补丁修的是**加载期**问题(不是写路径),它最危险的失效方式是"升级换了新树 ⇒ 补丁没跟着走 ⇒
|
|
6
|
+
* 某个旧会话突然打不开,而且没有运行期报错提示"(2026-10-17 本机就这么复现了一次)。故门禁要能
|
|
7
|
+
* 在**升级后立刻**判出"补丁在不在"。
|
|
8
|
+
*
|
|
9
|
+
* 判据两层:
|
|
10
|
+
* ① 结构(始终跑):目标模块里**确有**放宽逻辑(`Number.isSafeInteger(t)` + `t >= this.nextTurn` + 标记注释);
|
|
11
|
+
* 且**没有**退回成裸 `this.requireTurn(event.type)`。
|
|
12
|
+
* ② 行为(可选):给了 `DSH_TURN_OUTSIDE_SESSION=<一个 v4 日志路径>` 时,用真实加载校验
|
|
13
|
+
* (scripts/session-load-check.mjs,`validation:'current'`)证明它现在能过。
|
|
14
|
+
* 不给就**跳过并明说**(⛔ 不假装验过;本机已知样本:session-06732069 的 seq 30732)。
|
|
15
|
+
*
|
|
16
|
+
* 用法:node scripts/patches/verify-turn-outside-replace.mjs
|
|
17
|
+
* DSH_TURN_OUTSIDE_SESSION=~/.dsh/sessions/…/session.v4.jsonl.zstd node …(会展开 ~)
|
|
18
|
+
* 退出码:0 通过(或"结构过 + 行为跳过")/ 1 结构缺失 / 2 证据不足(scope 探不到)
|
|
19
|
+
*/
|
|
20
|
+
import { createRequire } from 'node:module'
|
|
21
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import { spawnSync } from 'node:child_process'
|
|
24
|
+
import { homedir } from 'node:os'
|
|
25
|
+
import { requireDshScope } from '../lib/dsh-scope.mjs'
|
|
26
|
+
|
|
27
|
+
let scope
|
|
28
|
+
try { scope = requireDshScope({ what: '补丁自检(verify-turn-outside-replace)' }) }
|
|
29
|
+
catch (e) { console.error(` ❌ ${e.message}`); process.exit(2) }
|
|
30
|
+
|
|
31
|
+
const target = join(scope, 'dsh-session-format-v3-to-v4', 'lib', 'index.js')
|
|
32
|
+
if (!existsSync(target)) {
|
|
33
|
+
console.error(` ❌ 目标模块不存在:${target}(上游可能改名/改结构)`)
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
const src = readFileSync(target, 'utf8')
|
|
37
|
+
|
|
38
|
+
let pass = 0
|
|
39
|
+
let fail = 0
|
|
40
|
+
const ok = (c, n) => { c ? (pass++, console.log(` ✅ ${n}`)) : (fail++, console.log(` ❌ ${n}`)) }
|
|
41
|
+
|
|
42
|
+
ok(/Argent hotfix(0\.2\.0-rc\.2 移植版/.test(src), '放宽逻辑的标记注释在位(说明"这是本仓库的补丁,不是上游原生行为")')
|
|
43
|
+
ok(/if \(this\.turn === null\) \{\s*const t = data\["turn"\];\s*if \(!Number\.isSafeInteger\(t\) \|\| t >= this\.nextTurn\) throw new SessionFormatError/.test(src),
|
|
44
|
+
'放宽口径在位:turn 外只要求 data.turn 是已关闭的历史 turn(安全整数且 < nextTurn)')
|
|
45
|
+
// 反向:不能退回成"一律要求打开 turn"(那样旧会话又会打不开)
|
|
46
|
+
ok(!/if \(event\.type === "tool\/result" && event\["surfaceOp"\] !== "append"\) \{\s*this\.requireTurn\(event\.type\);\s*return;/.test(src),
|
|
47
|
+
'没有退回成裸 requireTurn(退回去 = 旧会话重新打不开)')
|
|
48
|
+
|
|
49
|
+
// ② 行为(可选,不假装)
|
|
50
|
+
const fixture = (process.env.DSH_TURN_OUTSIDE_SESSION || '').replace(/^~(?=$|\/)/, homedir())
|
|
51
|
+
if (!fixture) {
|
|
52
|
+
console.log(' ⏭ 行为校验跳过:未给 DSH_TURN_OUTSIDE_SESSION(结构判据已覆盖"补丁在不在")')
|
|
53
|
+
} else if (!existsSync(fixture)) {
|
|
54
|
+
console.log(` ⏭ 行为校验跳过:DSH_TURN_OUTSIDE_SESSION 指向的文件不存在(${fixture})`)
|
|
55
|
+
} else {
|
|
56
|
+
const r = spawnSync(process.execPath, ['--max-old-space-size=8192', join(import.meta.dirname, '..', 'session-load-check.mjs'), fixture], {
|
|
57
|
+
encoding: 'utf8', env: { ...process.env, DSH_SCOPE: scope },
|
|
58
|
+
})
|
|
59
|
+
const out = `${r.stdout || ''}${r.stderr || ''}`
|
|
60
|
+
ok(r.status === 0, `行为校验:该会话能过真实加载校验(${fixture})${r.status === 0 ? '' : `\n ${out.trim().split('\n').slice(-4).join('\n ')}`}`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`\n${fail === 0 ? '✅' : '❌'} verify-turn-outside-replace:通过 ${pass} / 失败 ${fail}`)
|
|
64
|
+
process.exit(fail === 0 ? 0 : 1)
|
|
@@ -102,7 +102,7 @@ pick_py() {
|
|
|
102
102
|
[ -x "$c" ] || continue
|
|
103
103
|
"$c" -c 'import openpyxl, yaml' 2>/dev/null || continue # 硬要求:配置/发票生成必需
|
|
104
104
|
"$c" -c 'import PIL, playwright' 2>/dev/null || \
|
|
105
|
-
echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:
|
|
105
|
+
echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:bash scripts/python-runtime.sh ensure)" >&2
|
|
106
106
|
printf '%s' "$c"; return 0
|
|
107
107
|
done
|
|
108
108
|
return 1
|
|
@@ -518,7 +518,7 @@ function checkPackIntegrity() {
|
|
|
518
518
|
const msg = '未找到 install.json(可能未按能力清单安装)'
|
|
519
519
|
line('WARN', msg)
|
|
520
520
|
record('warn', 'pkg-install-json-missing', msg,
|
|
521
|
-
|
|
521
|
+
`按能力安装:由 @whyshu/argent 启动器安装(或核对 --dsh-home / ARGENT_INSTALL_JSON=${INSTALL_JSON})`)
|
|
522
522
|
return
|
|
523
523
|
}
|
|
524
524
|
const doc = readInstallJson(INSTALL_JSON)
|
|
@@ -23,9 +23,11 @@ case "$(uname -s 2>/dev/null || echo unknown)" in
|
|
|
23
23
|
esac
|
|
24
24
|
# P0-1(2026-10-08):体检项 4 → 5(补 selenium)。理由:官方唯一推荐的驱动档位
|
|
25
25
|
# (ZINIAO_MODE=selenium)此前在 venv 里**没装** ⇒ 选型开关静默撒谎、该档位从未运行过一次。
|
|
26
|
-
# ⚠️ 本清单必须与
|
|
26
|
+
# ⚠️ 本清单必须与 scripts/lib/python-runtime.mjs 的 venvUsable(pkgs=…) 和
|
|
27
27
|
# scripts/lib/python-runtime.mjs 的 venvUsable(pkgs=…) 保持一致(缺一就会"体检红但 ensure 空转")。
|
|
28
28
|
PKGS=(openpyxl yaml PIL playwright selenium)
|
|
29
|
+
# pip 安装名(import 名 ≠ 包名:yaml→pyyaml、PIL→pillow)
|
|
30
|
+
PIP_PKGS=(openpyxl pyyaml pillow playwright selenium)
|
|
29
31
|
# 交付纪律:本清单变更必须与"客户机 venv 补装 selenium(bash scripts/python-runtime.sh ensure)"
|
|
30
32
|
# 同批落地,否则客户机体检会先变红(4/5)。
|
|
31
33
|
|
|
@@ -53,10 +55,20 @@ cmd_check() {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
cmd_ensure() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
# 旧安装器退役(2026-10-18):直接创建持久 venv 并 pip 安装运行时依赖
|
|
59
|
+
local venv_dir; venv_dir="$(dirname "$VENV_BIN")"
|
|
60
|
+
local pybin="${PYTHON_BIN:-$(command -v python3 2>/dev/null || true)}"
|
|
61
|
+
if [ -z "$pybin" ] || [ ! -x "$pybin" ]; then
|
|
62
|
+
echo " ❌ 找不到 python3;请先安装 Python 3.10+" >&2; return 2
|
|
58
63
|
fi
|
|
59
|
-
|
|
64
|
+
if [ ! -x "$VENV_PY" ]; then
|
|
65
|
+
echo " → 创建 venv:$venv_dir"
|
|
66
|
+
"$pybin" -m venv "$venv_dir" || return 1
|
|
67
|
+
fi
|
|
68
|
+
echo " → 安装运行时依赖:${PIP_PKGS[*]}"
|
|
69
|
+
"$VENV_PY" -m pip install --upgrade pip >/dev/null 2>&1 || true
|
|
70
|
+
"$VENV_PY" -m pip install "${PIP_PKGS[@]}" || return $?
|
|
71
|
+
echo " ✅ venv 就绪:$VENV_PY"
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
cmd_path() {
|
|
@@ -69,7 +69,8 @@ REMOTE="${ARGENT_RELEASE_REMOTE:-$(git -C "$REPO_DIR" remote | head -1)}"
|
|
|
69
69
|
# 2026-10-09:补 install.sh —— 官网 macOS/Linux 一行命令 `curl .../dl/install.sh | bash` 的入口
|
|
70
70
|
# (旧的 `curl .../dl/install-argent.mjs | node` 因模块级相对 import 必然 ERR_MODULE_NOT_FOUND,
|
|
71
71
|
# 已被 install.sh 取代)。`install-dsh.sh` 留在这里只为**存量老命令**仍能取到,是**遗留**件。
|
|
72
|
-
|
|
72
|
+
# 旧一句话安装器已退役(2026-10-18):只保留安装说明页;启动器由 @whyshu/argent 承担
|
|
73
|
+
CORE_FILES=("argent-setup.md")
|
|
73
74
|
# 目录级分发(--full 时启用;服务器对应目录有独立静态资源,非同名覆盖不删旧)
|
|
74
75
|
DIR_FILES=("skills" "plugins")
|
|
75
76
|
|
|
@@ -184,7 +185,7 @@ pick_py() {
|
|
|
184
185
|
[ -x "$c" ] || continue
|
|
185
186
|
"$c" -c 'import openpyxl, yaml' 2>/dev/null || continue
|
|
186
187
|
"$c" -c 'import PIL, playwright' 2>/dev/null || \
|
|
187
|
-
echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒
|
|
188
|
+
echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:运行 @whyshu/argent 启动器自检子命令按提示补装;或手动 pip 安装)" >&2
|
|
188
189
|
printf '%s' "$c"; return 0
|
|
189
190
|
done
|
|
190
191
|
return 1
|
|
@@ -343,7 +344,7 @@ pick_py() {
|
|
|
343
344
|
[ -x "$c" ] || continue
|
|
344
345
|
"$c" -c 'import openpyxl, yaml' 2>/dev/null || continue # 硬要求:配置/发票生成必需
|
|
345
346
|
"$c" -c 'import PIL, playwright' 2>/dev/null || \
|
|
346
|
-
echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒
|
|
347
|
+
echo "[$(date '+%F %T')] ⚠️ 解释器 $c 缺 pillow/playwright ⇒ 浏览器流程会失败(修:运行 @whyshu/argent 启动器自检子命令按提示补装;或手动 pip 安装)" >&2
|
|
347
348
|
printf '%s' "$c"; return 0
|
|
348
349
|
done
|
|
349
350
|
return 1
|
|
@@ -537,7 +538,7 @@ if [[ $FULL -eq 1 ]]; then
|
|
|
537
538
|
else
|
|
538
539
|
echo " 3. (未指定 --full:skills/ plugins/ 目录不同步——服务器有独立静态资源)"
|
|
539
540
|
fi
|
|
540
|
-
echo " 4. 验证:本地/服务器 md5 + curl -sI ${DL_URL}/
|
|
541
|
+
echo " 4. 验证:本地/服务器 md5 + curl -sI ${DL_URL}/argent-setup.md"
|
|
541
542
|
|
|
542
543
|
if [[ $DRY_RUN -eq 1 ]]; then
|
|
543
544
|
echo
|
|
@@ -594,11 +595,11 @@ for f in "${CORE_FILES[@]}"; do
|
|
|
594
595
|
VERIFY_OK=0
|
|
595
596
|
fi
|
|
596
597
|
done
|
|
597
|
-
http_code="$(curl -sI -o /dev/null -w '%{http_code}' "${DL_URL}/
|
|
598
|
+
http_code="$(curl -sI -o /dev/null -w '%{http_code}' "${DL_URL}/argent-setup.md")"
|
|
598
599
|
if [[ "$http_code" == "200" ]]; then
|
|
599
|
-
echo " ✓ ${DL_URL}/
|
|
600
|
+
echo " ✓ ${DL_URL}/argent-setup.md HTTP ${http_code}"
|
|
600
601
|
else
|
|
601
|
-
echo " ✗ ${DL_URL}/
|
|
602
|
+
echo " ✗ ${DL_URL}/argent-setup.md HTTP ${http_code}(期望 200)"
|
|
602
603
|
VERIFY_OK=0
|
|
603
604
|
fi
|
|
604
605
|
|