@yixinkj/priority-buyer-alert-cli 0.1.0

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/index.js ADDED
@@ -0,0 +1,905 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import http from 'node:http';
6
+ import https from 'node:https';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { createRequire } from 'node:module';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const packageJson = require('./package.json');
15
+ const registry = require('./skills.json');
16
+
17
+ const VERSION = packageJson.version;
18
+ const SKILL_VERSION = registry.skill.version || VERSION;
19
+ const SKILL_ID = registry.skill.id;
20
+ const SKILL_DISPLAY_NAME = registry.skill.name;
21
+ const RUNTIME_VERSION = registry.runtimeVersion || VERSION;
22
+ const OSS_RELEASE_BASE_URL =
23
+ `https://broswer-plugs1.oss-cn-beijing.aliyuncs.com/releases/${SKILL_ID}/v${RUNTIME_VERSION}`;
24
+ const R2_RELEASE_BASE_URL =
25
+ `https://pub-60851fbb424a46c9a15b0ffce2ffea86.r2.dev/releases/${SKILL_ID}/v${RUNTIME_VERSION}`;
26
+ const RUNTIME_MANIFEST_SCHEMA = 'priority_buyer_alert_runtime_manifest_v1';
27
+ const PACKAGE_ROOT = path.dirname(fileURLToPath(import.meta.url));
28
+ // 已发布的包里 `skill/SKILL.md` 由 prepack 生成;直接在源码仓库里跑 index.js 时它不存在,
29
+ // 此时回退到仓库内的 Skill 源文件,避免联调必须先手工执行一次同步脚本。
30
+ // 发布产物中不存在仓库路径,因此这个回退不会让线上包读到仓库副本。
31
+ const PACKED_SKILL_PATH = path.join(PACKAGE_ROOT, 'skill', 'SKILL.md');
32
+ const REPOSITORY_SKILL_PATH = path.join(
33
+ PACKAGE_ROOT,
34
+ '..',
35
+ '..',
36
+ 'skills',
37
+ 'priority-buyer-alert',
38
+ 'SKILL.md'
39
+ );
40
+ const BUNDLED_SKILL_PATH = fs.existsSync(PACKED_SKILL_PATH)
41
+ ? PACKED_SKILL_PATH
42
+ : REPOSITORY_SKILL_PATH;
43
+ const SKILL_VERSION_MARKER = `.${SKILL_ID}-version.json`;
44
+ const SKILL_ROOT = path.join(os.homedir(), '.yixin', SKILL_ID);
45
+ const RUNTIME_ROOT = path.join(SKILL_ROOT, 'runtime');
46
+ const VERSION_ROOT = path.join(RUNTIME_ROOT, 'versions', `v${RUNTIME_VERSION}`);
47
+ const BIN_DIR = path.join(VERSION_ROOT, 'bin');
48
+ const INSTALL_MARKER = path.join(VERSION_ROOT, 'installed.json');
49
+
50
+ function fail(message) {
51
+ console.error(`[${SKILL_ID}] ${message}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ function emitStatus(status, message, details = {}) {
56
+ console.log(
57
+ JSON.stringify({ status, message, skill_version: SKILL_VERSION, ...details }, null, 2)
58
+ );
59
+ }
60
+
61
+ function parseSkillMetadata(content) {
62
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
63
+ if (!match) return {};
64
+ const metadata = {};
65
+ for (const line of match[1].split(/\r?\n/)) {
66
+ const field = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
67
+ if (!field) continue;
68
+ let value = field[2].trim();
69
+ if (
70
+ value.length >= 2 &&
71
+ ((value.startsWith('"') && value.endsWith('"')) ||
72
+ (value.startsWith("'") && value.endsWith("'")))
73
+ ) {
74
+ value = value.slice(1, -1);
75
+ }
76
+ metadata[field[1]] = value;
77
+ }
78
+ return metadata;
79
+ }
80
+
81
+ function parseSemver(version) {
82
+ const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
83
+ return match ? match.slice(1).map(Number) : null;
84
+ }
85
+
86
+ function compareVersions(left, right) {
87
+ const a = parseSemver(left);
88
+ const b = parseSemver(right);
89
+ if (!a && !b) return 0;
90
+ if (!a) return -1;
91
+ if (!b) return 1;
92
+ for (let index = 0; index < 3; index += 1) {
93
+ if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
94
+ }
95
+ return 0;
96
+ }
97
+
98
+ function extractSkillPath(args) {
99
+ let skillPath = null;
100
+ const forwardedArgs = [];
101
+ for (let index = 0; index < args.length; index += 1) {
102
+ const arg = args[index];
103
+ if (arg === '--skill-path') {
104
+ if (!args[index + 1]) throw new Error('--skill-path 缺少 SKILL.md 路径');
105
+ skillPath = args[index + 1];
106
+ index += 1;
107
+ continue;
108
+ }
109
+ if (arg.startsWith('--skill-path=')) {
110
+ skillPath = arg.slice('--skill-path='.length);
111
+ continue;
112
+ }
113
+ forwardedArgs.push(arg);
114
+ }
115
+ return { skillPath, forwardedArgs };
116
+ }
117
+
118
+ function isHelpRequest(args) {
119
+ return args.length === 0 || args.some((arg) => arg === '--help' || arg === '-h');
120
+ }
121
+
122
+ function printHelp() {
123
+ process.stdout.write(`${SKILL_DISPLAY_NAME} ${SKILL_VERSION}(Runtime ${RUNTIME_VERSION})
124
+
125
+ 用法:
126
+ ${SKILL_ID} --skill-path <SKILL.md> --start [扫描选项]
127
+ ${SKILL_ID} --skill-path <SKILL.md> --poll-run <RUN_ID>
128
+ ${SKILL_ID} --skill-path <SKILL.md> --run-status <RUN_ID>
129
+ ${SKILL_ID} --skill-path <SKILL.md> --cancel-run <RUN_ID>
130
+
131
+ 通用:
132
+ --skill-path <PATH> 本次实际读取的 SKILL.md 绝对路径
133
+ --version 显示版本
134
+ --help 显示本帮助
135
+
136
+ 扫描选项(仅与 --start 一起使用):
137
+ --minutes <N> 回复时限(SLA),默认 30
138
+ --lookback-hours <N> 只看最近 N 小时内有过消息的询盘,默认 24;0 表示不限
139
+ --max-pages <N> 翻页上限,每页 100 条,默认 3
140
+ --levels <LEVELS> 重点买家等级白名单,默认 L1+,L2,L3,L4,L5,L6
141
+ --re-alert <N> 升级重提间隔;不给则只提醒一次
142
+ --assignee <NAME> 只看某个负责人
143
+ --no-strict-bot 关闭机器人核验
144
+ --verify-all-replied 对全部已回复的重点买家做详情核验
145
+ --mark 推进提醒水位;只有心跳式调用才应使用
146
+
147
+ 环境变量:
148
+ PRIORITY_BUYER_ALERT_DETAIL_CONCURRENCY 详情核验并发度,默认 3,范围 1-8
149
+
150
+ 本 Skill 只读:只提醒,不发送消息、不修改任何页面状态。
151
+ `);
152
+ }
153
+
154
+ function normalizePath(filePath) {
155
+ const resolved = path.resolve(filePath);
156
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
157
+ }
158
+
159
+ function stripJsonComments(content) {
160
+ let output = '';
161
+ let inString = false;
162
+ let escaped = false;
163
+ let lineComment = false;
164
+ let blockComment = false;
165
+ for (let index = 0; index < content.length; index += 1) {
166
+ const char = content[index];
167
+ const next = content[index + 1];
168
+ if (lineComment) {
169
+ if (char === '\n') {
170
+ lineComment = false;
171
+ output += char;
172
+ }
173
+ continue;
174
+ }
175
+ if (blockComment) {
176
+ if (char === '*' && next === '/') {
177
+ blockComment = false;
178
+ index += 1;
179
+ }
180
+ continue;
181
+ }
182
+ if (inString) {
183
+ output += char;
184
+ if (escaped) escaped = false;
185
+ else if (char === '\\') escaped = true;
186
+ else if (char === '"') inString = false;
187
+ continue;
188
+ }
189
+ if (char === '"') {
190
+ inString = true;
191
+ output += char;
192
+ continue;
193
+ }
194
+ if (char === '/' && next === '/') {
195
+ lineComment = true;
196
+ index += 1;
197
+ continue;
198
+ }
199
+ if (char === '/' && next === '*') {
200
+ blockComment = true;
201
+ index += 1;
202
+ continue;
203
+ }
204
+ output += char;
205
+ }
206
+ return output.replace(/,\s*([}\]])/g, '$1');
207
+ }
208
+
209
+ function readSkillsRegistry(registryPath) {
210
+ return JSON.parse(stripJsonComments(fs.readFileSync(registryPath, 'utf8')));
211
+ }
212
+
213
+ function findRegistryEntry(data, skillDir) {
214
+ const target = normalizePath(skillDir);
215
+ return data.skills?.find(
216
+ (entry) => entry.installPath && normalizePath(entry.installPath) === target
217
+ );
218
+ }
219
+
220
+ function registryUpdatePreview(skillPath, metadata) {
221
+ const skillDir = path.dirname(skillPath);
222
+ const registryPath = path.join(skillDir, '..', 'skills.jsonc');
223
+ if (!fs.existsSync(registryPath)) {
224
+ return { registryPath: null, currentVersion: null, changed: false, content: null };
225
+ }
226
+ const data = readSkillsRegistry(registryPath);
227
+ const entry = findRegistryEntry(data, skillDir);
228
+ if (!entry) {
229
+ throw new Error(`skills.jsonc 未找到 installPath 对应项:${skillDir}`);
230
+ }
231
+ const before = JSON.stringify(entry);
232
+ const currentVersion = entry.version || null;
233
+ entry.name = metadata.name;
234
+ entry.description = metadata.description;
235
+ entry.version = SKILL_VERSION;
236
+ return {
237
+ registryPath,
238
+ currentVersion,
239
+ changed: before !== JSON.stringify(entry),
240
+ content: `${JSON.stringify(data, null, 2)}\n`
241
+ };
242
+ }
243
+
244
+ function snapshotFile(filePath) {
245
+ return fs.existsSync(filePath)
246
+ ? { exists: true, content: fs.readFileSync(filePath) }
247
+ : { exists: false, content: null };
248
+ }
249
+
250
+ function restoreFile(filePath, snapshot) {
251
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
252
+ if (snapshot.exists) fs.writeFileSync(filePath, snapshot.content);
253
+ else fs.rmSync(filePath, { force: true });
254
+ }
255
+
256
+ function readInstalledSkillVersion(skillDir, registryVersion) {
257
+ const markerPath = path.join(skillDir, SKILL_VERSION_MARKER);
258
+ try {
259
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
260
+ return marker.version || registryVersion || null;
261
+ } catch {
262
+ return registryVersion || null;
263
+ }
264
+ }
265
+
266
+ /**
267
+ * 把 npm 包内置的 SKILL.md 同步到当前智能体实际使用的安装位置。
268
+ * 本地版本更高时拒绝降级;任一校验失败时整体回滚,不留下半个新版 Skill。
269
+ * @param {string} skillPath 本次实际读取的 SKILL.md 绝对路径。
270
+ * @param {object} [options] 测试注入的内置文件路径。
271
+ * @returns {object} 同步结论与版本信息。
272
+ */
273
+ function syncSkillInstallation(skillPath, { bundledSkillPath = BUNDLED_SKILL_PATH } = {}) {
274
+ const resolvedSkillPath = path.resolve(skillPath);
275
+ if (path.basename(resolvedSkillPath).toLowerCase() !== 'skill.md') {
276
+ throw new Error('--skill-path 必须指向 SKILL.md');
277
+ }
278
+ if (!fs.existsSync(resolvedSkillPath)) {
279
+ throw new Error(`SKILL.md 不存在:${resolvedSkillPath}`);
280
+ }
281
+ if (!fs.existsSync(bundledSkillPath)) {
282
+ throw new Error('npm 包缺少内置 SKILL.md,请重新安装当前版本');
283
+ }
284
+
285
+ const bundledSkill = fs.readFileSync(bundledSkillPath, 'utf8');
286
+ const bundledMetadata = parseSkillMetadata(bundledSkill);
287
+ if (!bundledMetadata.name || !bundledMetadata.description) {
288
+ throw new Error('内置 SKILL.md 缺少 name 或 description');
289
+ }
290
+ if (bundledMetadata.name !== SKILL_ID) {
291
+ throw new Error('内置 SKILL.md 名称不匹配');
292
+ }
293
+
294
+ const currentSkill = fs.readFileSync(resolvedSkillPath, 'utf8');
295
+ if (parseSkillMetadata(currentSkill).name !== SKILL_ID) {
296
+ throw new Error(`--skill-path 指向的不是${SKILL_DISPLAY_NAME} Skill,已拒绝覆盖`);
297
+ }
298
+
299
+ const skillDir = path.dirname(resolvedSkillPath);
300
+ const registryPreview = registryUpdatePreview(resolvedSkillPath, bundledMetadata);
301
+ const currentVersion = readInstalledSkillVersion(skillDir, registryPreview.currentVersion);
302
+ if (compareVersions(currentVersion, SKILL_VERSION) > 0) {
303
+ return {
304
+ action: 'local_newer',
305
+ skillPath: resolvedSkillPath,
306
+ currentVersion,
307
+ targetVersion: SKILL_VERSION
308
+ };
309
+ }
310
+
311
+ const markerPath = path.join(skillDir, SKILL_VERSION_MARKER);
312
+ const markerContent = `${JSON.stringify({ version: SKILL_VERSION }, null, 2)}\n`;
313
+ const skillChanged = Buffer.from(currentSkill).compare(Buffer.from(bundledSkill)) !== 0;
314
+ const markerChanged =
315
+ !fs.existsSync(markerPath) || fs.readFileSync(markerPath, 'utf8') !== markerContent;
316
+
317
+ if (!skillChanged && !markerChanged && !registryPreview.changed) {
318
+ return {
319
+ action: 'current',
320
+ skillPath: resolvedSkillPath,
321
+ currentVersion,
322
+ targetVersion: SKILL_VERSION,
323
+ registryPath: registryPreview.registryPath
324
+ };
325
+ }
326
+
327
+ const snapshots = new Map([
328
+ [resolvedSkillPath, snapshotFile(resolvedSkillPath)],
329
+ [markerPath, snapshotFile(markerPath)]
330
+ ]);
331
+ if (registryPreview.registryPath) {
332
+ snapshots.set(registryPreview.registryPath, snapshotFile(registryPreview.registryPath));
333
+ }
334
+ try {
335
+ if (skillChanged) fs.writeFileSync(resolvedSkillPath, bundledSkill);
336
+ if (markerChanged) fs.writeFileSync(markerPath, markerContent);
337
+ if (registryPreview.changed) {
338
+ fs.writeFileSync(registryPreview.registryPath, registryPreview.content);
339
+ }
340
+ const writtenMetadata = parseSkillMetadata(fs.readFileSync(resolvedSkillPath, 'utf8'));
341
+ if (writtenMetadata.name !== bundledMetadata.name) {
342
+ throw new Error('写入后的 SKILL.md 名称校验失败');
343
+ }
344
+ if (JSON.parse(fs.readFileSync(markerPath, 'utf8')).version !== SKILL_VERSION) {
345
+ throw new Error('写入后的 Skill 版本标记校验失败');
346
+ }
347
+ if (registryPreview.registryPath) {
348
+ const writtenEntry = findRegistryEntry(
349
+ readSkillsRegistry(registryPreview.registryPath),
350
+ skillDir
351
+ );
352
+ if (
353
+ !writtenEntry ||
354
+ writtenEntry.version !== SKILL_VERSION ||
355
+ writtenEntry.name !== bundledMetadata.name ||
356
+ writtenEntry.description !== bundledMetadata.description
357
+ ) {
358
+ throw new Error('写入后的 skills.jsonc 校验失败');
359
+ }
360
+ }
361
+ } catch (error) {
362
+ for (const [filePath, snapshot] of snapshots) restoreFile(filePath, snapshot);
363
+ throw error;
364
+ }
365
+
366
+ return {
367
+ action: skillChanged ? 'updated' : 'registry_updated',
368
+ skillPath: resolvedSkillPath,
369
+ currentVersion,
370
+ targetVersion: SKILL_VERSION,
371
+ registryPath: registryPreview.registryPath
372
+ };
373
+ }
374
+
375
+ function discoverLegacySkillPaths(homeDir = os.homedir()) {
376
+ const accountsRoot = path.join(homeDir, '.accio', 'accounts');
377
+ if (!fs.existsSync(accountsRoot)) return [];
378
+ const candidates = [];
379
+ for (const account of fs.readdirSync(accountsRoot, { withFileTypes: true })) {
380
+ if (!account.isDirectory()) continue;
381
+ const agentsRoot = path.join(accountsRoot, account.name, 'agents');
382
+ if (!fs.existsSync(agentsRoot)) continue;
383
+ for (const agent of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
384
+ if (!agent.isDirectory()) continue;
385
+ const registryPath = path.join(
386
+ agentsRoot,
387
+ agent.name,
388
+ 'agent-core',
389
+ 'skills',
390
+ 'skills.jsonc'
391
+ );
392
+ if (!fs.existsSync(registryPath)) continue;
393
+ let data;
394
+ try {
395
+ data = readSkillsRegistry(registryPath);
396
+ } catch {
397
+ continue;
398
+ }
399
+ for (const entry of data.skills || []) {
400
+ if (!entry.installPath) continue;
401
+ const skillPath = path.join(entry.installPath, 'SKILL.md');
402
+ if (!fs.existsSync(skillPath)) continue;
403
+ let content;
404
+ try {
405
+ content = fs.readFileSync(skillPath, 'utf8');
406
+ } catch {
407
+ continue;
408
+ }
409
+ if (
410
+ content.includes(`@yixinkj/${SKILL_ID}-cli`) ||
411
+ new RegExp(`^name:\\s*${SKILL_ID}\\s*$`, 'm').test(content)
412
+ ) {
413
+ candidates.push(path.resolve(skillPath));
414
+ }
415
+ }
416
+ }
417
+ }
418
+ return [...new Set(candidates.map(normalizePath))];
419
+ }
420
+
421
+ function resolveSkillGate(skillPath, { homeDir = os.homedir(), syncOptions = {} } = {}) {
422
+ if (skillPath) {
423
+ const sync = syncSkillInstallation(skillPath, syncOptions);
424
+ return { allowRun: sync.action === 'current', sync };
425
+ }
426
+ const candidates = discoverLegacySkillPaths(homeDir);
427
+ if (candidates.length !== 1) {
428
+ return { allowRun: false, sync: { action: 'skill_path_required', candidates } };
429
+ }
430
+ const sync = syncSkillInstallation(candidates[0], syncOptions);
431
+ return {
432
+ allowRun: false,
433
+ sync: {
434
+ ...sync,
435
+ action: sync.action === 'local_newer' ? 'local_newer' : 'restart_required'
436
+ }
437
+ };
438
+ }
439
+
440
+ function detectTarget() {
441
+ const platform = registry.platforms[`${process.platform}-${process.arch}`];
442
+ if (!platform) {
443
+ const supported = Object.values(registry.platforms)
444
+ .map((entry) => entry.id)
445
+ .join('、');
446
+ throw new Error(
447
+ `暂不支持当前平台:${process.platform}/${process.arch}。已支持:${supported}。`
448
+ );
449
+ }
450
+ return { ...platform, binaries: [`${registry.skill.bin}-${platform.id}${platform.exe || ''}`] };
451
+ }
452
+
453
+ /**
454
+ * 生成业务运行时下载源,固定保持 OSS 主源优先、R2 备用源在后。
455
+ * @returns {string[]} 已清理、去重并保持优先级的下载根地址。
456
+ */
457
+ function releaseBaseUrls() {
458
+ const primary = process.env.PRIORITY_BUYER_ALERT_RELEASE_BASE_URL || OSS_RELEASE_BASE_URL;
459
+ const fallbacks = process.env.PRIORITY_BUYER_ALERT_FALLBACK_BASE_URLS
460
+ ? process.env.PRIORITY_BUYER_ALERT_FALLBACK_BASE_URLS.split(',')
461
+ : process.env.PRIORITY_BUYER_ALERT_RELEASE_BASE_URL
462
+ ? []
463
+ : [R2_RELEASE_BASE_URL];
464
+ return [
465
+ ...new Set(
466
+ [primary, ...fallbacks]
467
+ .filter(Boolean)
468
+ .map((value) => String(value).trim().replace(/\/$/, ''))
469
+ )
470
+ ];
471
+ }
472
+
473
+ function releaseUrls(target) {
474
+ return releaseBaseUrls().map((baseUrl) => `${baseUrl}/${target.archive}`);
475
+ }
476
+
477
+ function runtimeManifestUrls() {
478
+ return releaseBaseUrls().map((baseUrl) => `${baseUrl}/runtime-manifest.json`);
479
+ }
480
+
481
+ function runtimeManifestUrl() {
482
+ return runtimeManifestUrls()[0];
483
+ }
484
+
485
+ function sha256(filePath) {
486
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
487
+ }
488
+
489
+ function isInstalled(target) {
490
+ try {
491
+ const marker = JSON.parse(fs.readFileSync(INSTALL_MARKER, 'utf8'));
492
+ return (
493
+ marker.version === RUNTIME_VERSION &&
494
+ marker.platform === target.id &&
495
+ target.binaries.every((name) => {
496
+ const expected = marker.files?.[name];
497
+ const filePath = path.join(BIN_DIR, name);
498
+ return (
499
+ expected &&
500
+ fs.existsSync(filePath) &&
501
+ fs.statSync(filePath).size === expected.size &&
502
+ sha256(filePath) === expected.sha256
503
+ );
504
+ })
505
+ );
506
+ } catch {
507
+ return false;
508
+ }
509
+ }
510
+
511
+ function download(url, outputPath, redirects = 5) {
512
+ return new Promise((resolve, reject) => {
513
+ const client = url.startsWith('http:') ? http : https;
514
+ const request = client.get(
515
+ url,
516
+ { headers: { 'User-Agent': `@yixinkj/${SKILL_ID}-cli` } },
517
+ (response) => {
518
+ if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
519
+ response.resume();
520
+ if (!response.headers.location || redirects === 0) {
521
+ reject(new Error(`下载重定向过多:${url}`));
522
+ return;
523
+ }
524
+ download(
525
+ new URL(response.headers.location, url).toString(),
526
+ outputPath,
527
+ redirects - 1
528
+ ).then(resolve, reject);
529
+ return;
530
+ }
531
+ if (response.statusCode !== 200) {
532
+ response.resume();
533
+ reject(new Error(`下载失败:HTTP ${response.statusCode} ${url}`));
534
+ return;
535
+ }
536
+ const file = fs.createWriteStream(outputPath);
537
+ response.pipe(file);
538
+ response.on('aborted', () => {
539
+ file.destroy();
540
+ reject(new Error(`下载连接意外中断:${url}`));
541
+ });
542
+ response.on('error', (error) => file.destroy(error));
543
+ file.on('finish', () => file.close(resolve));
544
+ file.on('error', reject);
545
+ }
546
+ );
547
+ request.setTimeout(60000, () => request.destroy(new Error(`下载超时:${url}`)));
548
+ request.on('error', reject);
549
+ });
550
+ }
551
+
552
+ /**
553
+ * 从同一个来源成对下载清单和压缩包,并在切换来源前完成版本、平台和 SHA256 校验。
554
+ * 清单与文件必须同源处理,避免主源清单与备用源文件版本漂移时被错误组合。
555
+ * @param {object} target 当前平台的归档名称和标识。
556
+ * @param {string} releaseManifestPath 临时清单文件路径。
557
+ * @param {string} archivePath 临时运行时归档路径。
558
+ * @param {object} [options] 下载源和测试注入选项。
559
+ * @returns {Promise<object>} 与归档同源且已通过校验的运行时清单。
560
+ */
561
+ async function downloadVerifiedRelease(
562
+ target,
563
+ releaseManifestPath,
564
+ archivePath,
565
+ { baseUrls = releaseBaseUrls(), downloadFile = download } = {}
566
+ ) {
567
+ const failures = [];
568
+ for (const baseUrl of baseUrls) {
569
+ const root = String(baseUrl).trim().replace(/\/$/, '');
570
+ try {
571
+ fs.rmSync(releaseManifestPath, { force: true });
572
+ fs.rmSync(archivePath, { force: true });
573
+ await downloadFile(`${root}/runtime-manifest.json`, releaseManifestPath);
574
+ const manifest = JSON.parse(fs.readFileSync(releaseManifestPath, 'utf8'));
575
+ await downloadFile(`${root}/${target.archive}`, archivePath);
576
+ verifyReleaseArchive(manifest, target, archivePath);
577
+ return manifest;
578
+ } catch (error) {
579
+ fs.rmSync(releaseManifestPath, { force: true });
580
+ fs.rmSync(archivePath, { force: true });
581
+ failures.push(`${root}: ${error.message}`);
582
+ }
583
+ }
584
+ throw new Error(`所有下载源均未通过运行时校验:\n${failures.join('\n')}`);
585
+ }
586
+
587
+ function verifyReleaseArchive(manifest, target, archivePath) {
588
+ if (
589
+ manifest?.schema !== RUNTIME_MANIFEST_SCHEMA ||
590
+ manifest.runtime_version !== RUNTIME_VERSION
591
+ ) {
592
+ throw new Error('公共运行时清单版本与当前启动器不一致');
593
+ }
594
+ const expected = manifest.platforms?.[target.id];
595
+ if (!expected || expected.archive !== target.archive) {
596
+ throw new Error(`公共运行时清单缺少平台 ${target.id}`);
597
+ }
598
+ const stat = fs.statSync(archivePath);
599
+ if (stat.size !== expected.size || sha256(archivePath) !== expected.sha256) {
600
+ throw new Error(`公共运行时压缩包校验失败:${target.archive}`);
601
+ }
602
+ }
603
+
604
+ function runCommand(command, args) {
605
+ const result = spawnSync(command, args, {
606
+ encoding: 'utf8',
607
+ stdio: ['ignore', 'pipe', 'pipe'],
608
+ windowsHide: true
609
+ });
610
+ if (result.error) throw result.error;
611
+ if (result.status !== 0) {
612
+ const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
613
+ throw new Error(detail || `${command} 退出码为 ${result.status}`);
614
+ }
615
+ }
616
+
617
+ function extractArchive(archivePath, destination) {
618
+ fs.mkdirSync(destination, { recursive: true });
619
+ if (archivePath.endsWith('.tar.gz')) {
620
+ runCommand('tar', ['-xzf', archivePath, '-C', destination]);
621
+ return;
622
+ }
623
+ if (archivePath.endsWith('.zip')) {
624
+ if (process.platform === 'win32') {
625
+ runCommand('powershell.exe', [
626
+ '-NoProfile',
627
+ '-ExecutionPolicy',
628
+ 'Bypass',
629
+ '-Command',
630
+ `Expand-Archive -LiteralPath ${JSON.stringify(archivePath)} -DestinationPath ${JSON.stringify(destination)} -Force`
631
+ ]);
632
+ return;
633
+ }
634
+ runCommand('unzip', ['-q', archivePath, '-d', destination]);
635
+ return;
636
+ }
637
+ throw new Error(`暂不支持的压缩包类型:${archivePath}`);
638
+ }
639
+
640
+ function verifyExtracted(extractedDir, target) {
641
+ const manifest = JSON.parse(
642
+ fs.readFileSync(path.join(extractedDir, 'checksums.json'), 'utf8')
643
+ );
644
+ if (manifest.version !== RUNTIME_VERSION || manifest.platform !== target.id) {
645
+ throw new Error('下载包版本或平台与当前启动器不一致');
646
+ }
647
+ for (const name of target.binaries) {
648
+ const expected = manifest.files?.[name];
649
+ const filePath = path.join(extractedDir, name);
650
+ if (
651
+ !expected ||
652
+ !fs.existsSync(filePath) ||
653
+ fs.statSync(filePath).size !== expected.size ||
654
+ sha256(filePath) !== expected.sha256
655
+ ) {
656
+ throw new Error(`下载包校验失败:${name}`);
657
+ }
658
+ }
659
+ return manifest.files;
660
+ }
661
+
662
+ function prepareMacBinaries(target) {
663
+ if (process.platform !== 'darwin') return;
664
+ for (const name of target.binaries) {
665
+ const filePath = path.join(BIN_DIR, name);
666
+ spawnSync('xattr', ['-dr', 'com.apple.quarantine', filePath], { stdio: 'ignore' });
667
+ runCommand('chmod', ['+x', filePath]);
668
+ }
669
+ }
670
+
671
+ async function ensureInstalled(target) {
672
+ if (isInstalled(target)) {
673
+ prepareMacBinaries(target);
674
+ return;
675
+ }
676
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `${SKILL_ID}-install-`));
677
+ const archivePath = path.join(tempRoot, target.archive);
678
+ const releaseManifestPath = path.join(tempRoot, 'runtime-manifest.json');
679
+ const extractedDir = path.join(tempRoot, 'extracted');
680
+ const stagingRoot = `${VERSION_ROOT}.staging-${process.pid}`;
681
+ try {
682
+ await downloadVerifiedRelease(target, releaseManifestPath, archivePath);
683
+ extractArchive(archivePath, extractedDir);
684
+ const files = verifyExtracted(extractedDir, target);
685
+
686
+ fs.mkdirSync(path.dirname(VERSION_ROOT), { recursive: true });
687
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
688
+ fs.mkdirSync(path.join(stagingRoot, 'bin'), { recursive: true });
689
+ for (const name of target.binaries) {
690
+ fs.copyFileSync(path.join(extractedDir, name), path.join(stagingRoot, 'bin', name));
691
+ }
692
+ fs.writeFileSync(
693
+ path.join(stagingRoot, 'installed.json'),
694
+ `${JSON.stringify({ version: RUNTIME_VERSION, platform: target.id, files }, null, 2)}\n`
695
+ );
696
+
697
+ fs.rmSync(VERSION_ROOT, { recursive: true, force: true });
698
+ fs.renameSync(stagingRoot, VERSION_ROOT);
699
+ prepareMacBinaries(target);
700
+ } finally {
701
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
702
+ fs.rmSync(tempRoot, { recursive: true, force: true });
703
+ }
704
+ }
705
+
706
+ async function ensureSharedBridge() {
707
+ let manager;
708
+ try {
709
+ manager = await import('@yixinkj/yixin-bridge-cli');
710
+ } catch (error) {
711
+ throw new Error(`缺少共享译心桥管理器 @yixinkj/yixin-bridge-cli:${error.message}`);
712
+ }
713
+ const result = await manager.ensureBridgeRuntime({ minVersion: registry.bridge.minVersion });
714
+ if (!result?.bridge_path || !fs.existsSync(result.bridge_path)) {
715
+ throw new Error('共享译心桥安装完成后未返回有效运行路径');
716
+ }
717
+ validateBridgeCapabilities(result, registry.bridge.requiredCapabilities || []);
718
+ return result;
719
+ }
720
+
721
+ /**
722
+ * 校验当前共享桥是否具备本 Skill 的硬依赖能力。
723
+ * @param {object} result 共享桥管理器返回的运行时信息。
724
+ * @param {string[]} requiredCapabilities 本 Skill 声明的能力白名单。
725
+ * @returns {object} 校验通过后的原始运行时信息。
726
+ */
727
+ function validateBridgeCapabilities(result, requiredCapabilities) {
728
+ const missing = requiredCapabilities.filter(
729
+ (capability) => !result.capabilities?.includes(capability)
730
+ );
731
+ if (missing.length > 0) {
732
+ // 具体 Skill 只校验自己声明的能力,避免无关运行时能力成为全局硬依赖。
733
+ throw new Error(`共享译心桥缺少必要能力:${missing.join(', ')}`);
734
+ }
735
+ return result;
736
+ }
737
+
738
+ /**
739
+ * 把共享管理器的底层插件状态转换为用户可理解的有限提示。
740
+ * @param {object | null} sharedBridge 共享桥管理器返回的安装和插件状态。
741
+ * @returns {object | null} 需要展示的提示结构;无需提示时返回空值。
742
+ */
743
+ function browserExtensionNotice(sharedBridge) {
744
+ if (!sharedBridge) return null;
745
+ const reloadStatus = sharedBridge.extension_reload?.status || 'not_requested';
746
+ const details = {
747
+ extension_version: sharedBridge.extension_version || null,
748
+ extension_path: sharedBridge.extension_path || null,
749
+ reload_status: reloadStatus
750
+ };
751
+ if (sharedBridge.extension_updated) {
752
+ return reloadStatus === 'reloaded'
753
+ ? {
754
+ ...details,
755
+ status: 'updated_reloaded',
756
+ message: `浏览器插件已更新至 ${details.extension_version || '最新版'},并已自动重新加载。`
757
+ }
758
+ : {
759
+ ...details,
760
+ status: 'manual_reload_required',
761
+ message: `浏览器插件已更新至 ${details.extension_version || '最新版'},但未能自动重新加载。请在 Chrome 扩展管理页手动重新加载。`
762
+ };
763
+ }
764
+ if (reloadStatus === 'active_tasks_present') {
765
+ return {
766
+ ...details,
767
+ status: 'update_deferred',
768
+ message: '检测到浏览器插件更新,但当前仍有采集任务,已暂缓更新。任务结束后请重新运行。'
769
+ };
770
+ }
771
+ if (reloadStatus === 'daemon_status_unavailable') {
772
+ return {
773
+ ...details,
774
+ status: 'update_deferred',
775
+ message: '检测到浏览器插件更新,但暂时无法确认浏览器任务状态,未自动替换。请稍后重新运行。'
776
+ };
777
+ }
778
+ return null;
779
+ }
780
+
781
+ function emitBrowserExtensionNotice(sharedBridge) {
782
+ const notice = browserExtensionNotice(sharedBridge);
783
+ if (!notice) return;
784
+ // 原生 CLI 的 stdout 是机器解析的 JSON,因此提示只能写 stderr。
785
+ console.error(`[${SKILL_ID}] ${notice.message}`);
786
+ if (notice.extension_path) {
787
+ console.error(`[${SKILL_ID}] 浏览器插件本地路径:${notice.extension_path}`);
788
+ }
789
+ }
790
+
791
+ async function main() {
792
+ const rawArgs = process.argv.slice(2);
793
+ if (isHelpRequest(rawArgs)) {
794
+ printHelp();
795
+ return;
796
+ }
797
+ const { skillPath, forwardedArgs } = extractSkillPath(rawArgs);
798
+ if (forwardedArgs.length === 1 && ['--version', '-V', '-v'].includes(forwardedArgs[0])) {
799
+ console.log(`${SKILL_ID} ${SKILL_VERSION} (runtime ${RUNTIME_VERSION})`);
800
+ return;
801
+ }
802
+
803
+ console.error(`[${SKILL_ID}] ${SKILL_DISPLAY_NAME} ${SKILL_VERSION}`);
804
+ const gate = resolveSkillGate(skillPath);
805
+ if (!gate.allowRun) {
806
+ if (gate.sync.action === 'local_newer') {
807
+ emitStatus(
808
+ 'skill_version_conflict',
809
+ `当前 Skill 版本 ${gate.sync.currentVersion} 高于 npm 版本 ${SKILL_VERSION},已停止运行,未执行降级。`,
810
+ { skill_path: gate.sync.skillPath }
811
+ );
812
+ process.exitCode = 1;
813
+ return;
814
+ }
815
+ if (gate.sync.action === 'skill_path_required') {
816
+ emitStatus(
817
+ 'skill_path_required',
818
+ '无法唯一确定当前智能体的 SKILL.md。请使用本次读取的 SKILL.md 绝对路径重新运行;本次未执行扫描。',
819
+ { candidates: gate.sync.candidates }
820
+ );
821
+ return;
822
+ }
823
+ emitStatus(
824
+ 'skill_updated',
825
+ `${SKILL_DISPLAY_NAME}已更新至 ${SKILL_VERSION}。请重新试一次,本次不执行扫描。`,
826
+ {
827
+ skill_path: gate.sync.skillPath,
828
+ previous_version: gate.sync.currentVersion || null,
829
+ skills_registry: gate.sync.registryPath || null,
830
+ restart_required: true
831
+ }
832
+ );
833
+ return;
834
+ }
835
+
836
+ const runtimeOverride = process.env.PRIORITY_BUYER_ALERT_RUNTIME_BINARY;
837
+ if (runtimeOverride) {
838
+ const binary = path.resolve(runtimeOverride);
839
+ if (!fs.existsSync(binary)) throw new Error(`开发运行时不存在:${binary}`);
840
+ const result = spawnSync(binary, forwardedArgs, {
841
+ stdio: 'inherit',
842
+ windowsHide: true,
843
+ env: process.env
844
+ });
845
+ if (result.error) throw result.error;
846
+ process.exitCode = result.status ?? 1;
847
+ return;
848
+ }
849
+
850
+ const target = detectTarget();
851
+ const [sharedBridge] = await Promise.all([ensureSharedBridge(), ensureInstalled(target)]);
852
+ emitBrowserExtensionNotice(sharedBridge);
853
+ const result = spawnSync(path.join(BIN_DIR, target.binaries[0]), forwardedArgs, {
854
+ stdio: 'inherit',
855
+ windowsHide: true,
856
+ env: { ...process.env, YIXIN_BRIDGE_BIN: sharedBridge.bridge_path }
857
+ });
858
+ if (result.error) throw result.error;
859
+ process.exitCode = result.status ?? 1;
860
+ }
861
+
862
+ function isDirectExecution() {
863
+ if (!process.argv[1]) return false;
864
+ let invoked = path.resolve(process.argv[1]);
865
+ let current = fileURLToPath(import.meta.url);
866
+ try {
867
+ invoked = fs.realpathSync(invoked);
868
+ current = fs.realpathSync(current);
869
+ } catch {
870
+ // 某些文件系统无法解析符号链接时使用绝对路径比较。
871
+ }
872
+ return process.platform === 'win32'
873
+ ? invoked.toLowerCase() === current.toLowerCase()
874
+ : invoked === current;
875
+ }
876
+
877
+ if (isDirectExecution()) {
878
+ main().catch((error) => fail(error.message));
879
+ }
880
+
881
+ export {
882
+ BIN_DIR,
883
+ BUNDLED_SKILL_PATH,
884
+ RUNTIME_VERSION,
885
+ RUNTIME_ROOT,
886
+ SKILL_ROOT,
887
+ SKILL_VERSION,
888
+ VERSION_ROOT,
889
+ browserExtensionNotice,
890
+ compareVersions,
891
+ detectTarget,
892
+ discoverLegacySkillPaths,
893
+ downloadVerifiedRelease,
894
+ extractSkillPath,
895
+ isHelpRequest,
896
+ isInstalled,
897
+ parseSkillMetadata,
898
+ releaseUrls,
899
+ resolveSkillGate,
900
+ runtimeManifestUrl,
901
+ runtimeManifestUrls,
902
+ syncSkillInstallation,
903
+ validateBridgeCapabilities,
904
+ verifyReleaseArchive
905
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@yixinkj/priority-buyer-alert-cli",
3
+ "version": "0.1.0",
4
+ "description": "重点买家未回复预警独立启动器,只读扫描询盘并提醒,不发送任何消息。",
5
+ "type": "module",
6
+ "bin": {
7
+ "priority-buyer-alert": "index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test index.test.js",
11
+ "prepack": "node scripts/sync-skill-assets.mjs",
12
+ "postpack": "node scripts/clean-skill-assets.mjs"
13
+ },
14
+ "files": [
15
+ "index.js",
16
+ "skills.json",
17
+ "skill"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "dependencies": {
23
+ "@yixinkj/yixin-bridge-cli": "^1.0.6"
24
+ },
25
+ "license": "UNLICENSED",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: priority-buyer-alert
3
+ description: 重点买家未回复预警。只读扫描阿里国际站询盘列表,挑出 L2 及以上(出口通店铺为 L1+)的重点买家,判断买家最后一句是否超过 SLA 仍没有真人业务员回复,并输出中文预警清单;机器人自动接待不算人工回复。Use when 用户要求检查重点买家有没有超时未回复、查询盘超时提醒、看 L3/L4 有没有快凉的,或要求运行重点买家预警。
4
+ version: "0.1.0"
5
+ ---
6
+
7
+ # 重点买家预警
8
+
9
+ ## 必须遵守
10
+
11
+ - 本 Skill 只读:只提醒,永不替人发消息、不改负责人、不改询盘状态、不提交任何表单。
12
+ - 只使用下方 npm CLI、统一授权和共享译心桥;不调用其他浏览器代理,不扫描工作区。
13
+ - 预警成立的唯一口径由 CLI 判定,模型不得自行改写:买家最后一句之后没有真人业务员回复,且等待时间严格超过 SLA。
14
+ - 机器人自动接待和系统通知都不算人工回复。
15
+ - 缺买家等级的询盘不算重点、不冒泡,只计入数据提醒。
16
+ - 计数、排序和等待分钟数全部使用 CLI 产物,禁止模型自行统计或估算。
17
+ - 没有预警时必须明确说明当前范围内没有超时未回复的重点买家,不得编造。
18
+
19
+ ## 运行入口
20
+
21
+ ```bash
22
+ npx -y @yixinkj/priority-buyer-alert-cli@latest \
23
+ --skill-path "本次实际读取的 SKILL.md 绝对路径" \
24
+ --start
25
+ ```
26
+
27
+ 启动成功后立即记录返回的 `run_id`,此后只用同一个 `run_id` 轮询,不要再次执行 `--start`。
28
+
29
+ 常用参数(都只能与 `--start` 一起使用):
30
+
31
+ - `--minutes 30`:SLA,超过多少分钟没有人工回复算超时。默认 30,严格大于才触发。
32
+ - `--lookback-hours 24`:只看最近多少小时内有过消息的询盘。默认 24。询盘列表按最近联系时间倒序,因此翻到某页最旧一行早于窗口就可以安全停止,心跳通常只读一页。传 0 表示不按时间停止翻页。
33
+ - `--max-pages 3`:翻页上限,每页 100 条,给忙店铺兜底。
34
+ - `--levels "L2,L3,L4"`:收紧重点等级。不传时默认 `L1+,L2,L3,L4,L5,L6`,即「等级存在且高于 L1」,金品店铺和出口通店铺都适用。
35
+ - `--assignee "业务员名"`:只看某个负责人。
36
+ - `--re-alert 30`:仍未回复时每隔多少分钟升级重提。不传表示同一轮等待只提醒一次。
37
+ - `--mark`:推进提醒水位与升级计数。只有无人值守的心跳式调用才使用;人工查看不要带。
38
+ - `--verify-all-replied`:对全部「末条是我方发出」的重点买家都做详情核验,更慢更全。
39
+
40
+ ### 关于买家等级
41
+
42
+ - 金品店铺可以看到真实档 L1–L6。
43
+ - 出口通店铺被阿里统一脱敏,L1 以上一律显示为整体值 `L1+`,采集端无法还原真实档。
44
+ - 默认白名单同时包含 `L1+` 和 L2–L6,因此不需要先判断店铺套餐。显式传 `--levels` 时按值精确匹配,`L1+` 必须原样带引号传入。
45
+
46
+ ## 轮询
47
+
48
+ 按返回的 `poll_after_seconds` 静默轮询,不逐条播报进度:
49
+
50
+ ```bash
51
+ npx -y @yixinkj/priority-buyer-alert-cli@latest \
52
+ --skill-path "本次实际读取的 SKILL.md 绝对路径" \
53
+ --poll-run "run_id"
54
+ ```
55
+
56
+ - `scanning`、`verifying`:继续轮询同一个 `run_id`。
57
+ - exit 124 或前台超时:继续轮询同一个 `run_id`,不要重启任务。
58
+ - `completed`:读取 `artifacts.digest` 交付。
59
+
60
+ 查看但不推进:`--run-status "run_id"`。用户明确要求停止时:`--cancel-run "run_id"`。
61
+
62
+ ## 交付
63
+
64
+ 读取 `artifacts.digest` 指向的 Markdown 文件,按其内容向用户转述:
65
+
66
+ - 没有预警时只说明本轮扫描范围和「没有超时未回复的重点买家」。
67
+ - 有预警时按文件已有顺序逐条转述:谁的(负责人)、哪个买家和国家、哪一档等级、已等待多久、买家最后那句话。顺序已按等级和等待时长排好,不要重排。
68
+ - 语气是「这条重点客户该接了」,不是追责。`提醒次数` 达到 3 次以上时可以建议同步主管。
69
+ - 每条可以顺带提议起草回复,但只建议、不替发。
70
+ - 文件末尾的「数据提醒」如实转述,不要把没抓到等级的询盘当成重点买家。其中「没能取回买家资料」和「因超过单轮核验上限没有核验」都意味着本轮结论没有覆盖这些询盘,不能说成「已确认没有风险」。
71
+ - 「盲区大小」那一条是当前等级口径没有覆盖的等待买家数量。如实转述,但不要据此自行把它们当作预警冒泡。
72
+ - 不输出原始 JSON、绝对路径、买家邮箱电话或任何 token。
73
+
74
+ ## 定时与推送
75
+
76
+ 本 Skill 不自建定时任务,也不自己发消息到任何群。无人值守时由你按固定间隔(建议取 SLA 的三分之一到二分之一,例如 SLA 30 分钟就每 10–15 分钟一次)执行下面这条心跳命令:
77
+
78
+ ```bash
79
+ npx -y @yixinkj/priority-buyer-alert-cli@latest \
80
+ --skill-path "本次实际读取的 SKILL.md 绝对路径" \
81
+ --start --minutes 30 --re-alert 30 --mark
82
+ ```
83
+
84
+ 心跳必须带 `--mark`,否则同一条会每轮重复提醒。轮询到 `completed` 后:
85
+
86
+ - `summary.alerts_reported == 0`:本轮没有需要冒泡的重点买家,**静默结束,不要推送任何消息**,也不要向用户汇报「本次没有预警」。
87
+ - `summary.alerts_reported > 0`:读取 `artifacts.digest`,把预警明细整理成一条中文消息推送到钉钉或飞书群。
88
+
89
+ 推送消息的要求:
90
+
91
+ - 一条消息里按 digest 已有顺序列全,不要拆成多条刷屏、不要重排。
92
+ - 每条包含负责人、买家和国家、等级、已等待分钟数、买家最后那句话。
93
+ - 提醒次数 ≥ 3 的单独标出并建议同步主管。
94
+ - 不要把 `数据提醒` 里的采集问题推给业务群;那属于运维信息,只在用户主动询问时说明。
95
+ - 推送失败时如实报告失败,不要重复推送同一批预警;水位已经推进过,重复触发只会造成漏报错觉。
96
+
97
+ ## 异常
98
+
99
+ - `authorization_failed`:停止并提示用户先完成译心授权。
100
+ - `bridge_offline`:停止并提示用户打开浏览器、确认译心插件已连接后重试。
101
+ - `login_required`:停止本轮,提示用户在浏览器重新登录后重跑;不得读取旧产物反推结论。
102
+ - `account_risk_detected`:停止并提示用户先在页面完成安全校验。
103
+ - `browser_profile_ambiguous`:请用户只保留目标 Profile 已连接,或只在目标 Profile 打开消息中心后重试。
104
+ - `browser_profile_unavailable`、`browser_tab_closed`:请用户打开阿里国际站消息中心页面后重试。
105
+ - `account_switched`:硬停止,禁止跨账号继续。
106
+ - `schema_changed`:停止,不猜字段、不改用其他入口,如实告知页面结构已变化。
107
+ - `run_schema_outdated`:该 `run_id` 是旧版本 CLI 创建的,不能继续推进。重新执行一次 `--start` 取得新的 `run_id`,不要修改或删除本地任何状态文件。
108
+ - 浏览器插件已更新但需要手动加载时,把 CLI 返回的 `extension_path` 渲染为 `[打开浏览器插件目录](<绝对路径>)` 可点击链接。
109
+ - `skill_updated`:只回复「重点买家预警已更新,请重新试一次。」并结束。
110
+ - npm 返回 `ETARGET` 或 `No matching version found` 时,只执行一次 `npm view @yixinkj/priority-buyer-alert-cli dist-tags versions --json --prefer-online`,再用原参数重跑一次;仍失败则停止,不猜版本、不降级。
111
+ - 其他失败只转述 CLI 的 `error_code` 和 `message`,不自行寻找替代入口。
112
+ - 心跳模式下发生任何错误:停止本轮并如实上报,不要退化成推送旧结果,也不要把失败当成「没有预警」。
package/skills.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "runtimeVersion": "0.1.0",
3
+ "platforms": {
4
+ "darwin-arm64": {
5
+ "id": "mac-arm64",
6
+ "archive": "priority-buyer-alert-mac-arm64.tar.gz",
7
+ "exe": ""
8
+ },
9
+ "darwin-x64": {
10
+ "id": "mac-x64",
11
+ "archive": "priority-buyer-alert-mac-x64.tar.gz",
12
+ "exe": ""
13
+ },
14
+ "win32-x64": {
15
+ "id": "win32-x64",
16
+ "archive": "priority-buyer-alert-win32-x64.zip",
17
+ "exe": ".exe"
18
+ }
19
+ },
20
+ "bridge": {
21
+ "package": "@yixinkj/yixin-bridge-cli",
22
+ "minVersion": "1.0.6",
23
+ "requiredCapabilities": ["frame_url_v1"]
24
+ },
25
+ "skill": {
26
+ "name": "重点买家预警",
27
+ "id": "priority-buyer-alert",
28
+ "version": "0.1.0",
29
+ "bin": "priority-buyer-alert-cli"
30
+ }
31
+ }