@yixinkj/priority-buyer-alert-cli 0.1.42 → 0.1.44
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 +222 -778
- package/launcher-core.js +1457 -0
- package/package.json +4 -3
- package/skill/SKILL.md +37 -11
- package/skills.json +2 -2
package/index.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
3
|
import fs from 'node:fs';
|
|
5
|
-
import http from 'node:http';
|
|
6
|
-
import https from 'node:https';
|
|
7
4
|
import os from 'node:os';
|
|
8
5
|
import path from 'node:path';
|
|
9
6
|
import { spawnSync } from 'node:child_process';
|
|
10
7
|
import { createRequire } from 'node:module';
|
|
11
8
|
import { fileURLToPath } from 'node:url';
|
|
12
9
|
|
|
10
|
+
// 三份启动器里逐字相同的那 700 行现在住在 launcher-core.js。
|
|
11
|
+
// 它不读取任何「我是谁」的信息:SKILL_ID、版本、版本目录、日志前缀、环境变量名、
|
|
12
|
+
// import.meta.url 全部由下面的绑定层显式传入,漏传就在内核入口抛一句中文错误。
|
|
13
|
+
// 为什么必须这样:见 launcher-core-migration.md §1(SKILL_ID is not defined 事故)。
|
|
14
|
+
// 这个文件怎么进到别的仓、怎么保证四份一致:见 launcher-core-distribution.md。
|
|
15
|
+
// 注意:它必须出现在 package.json 的 files 里,否则发布出去的包在 npx 第一行就
|
|
16
|
+
// ERR_MODULE_NOT_FOUND。package-contents.test.js 用真实的 npm pack 清单守着这一条。
|
|
17
|
+
import * as core from './launcher-core.js';
|
|
18
|
+
|
|
13
19
|
const require = createRequire(import.meta.url);
|
|
14
20
|
const packageJson = require('./package.json');
|
|
15
21
|
const registry = require('./skills.json');
|
|
@@ -49,257 +55,155 @@ const VERSION_ROOT = path.join(RUNTIME_ROOT, 'versions', `v${RUNTIME_VERSION}`);
|
|
|
49
55
|
const BIN_DIR = path.join(VERSION_ROOT, 'bin');
|
|
50
56
|
const INSTALL_MARKER = path.join(VERSION_ROOT, 'installed.json');
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
return metadata;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function parseSemver(version) {
|
|
84
|
-
const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
85
|
-
return match ? match.slice(1).map(Number) : null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function compareVersions(left, right) {
|
|
89
|
-
const a = parseSemver(left);
|
|
90
|
-
const b = parseSemver(right);
|
|
91
|
-
if (!a && !b) return 0;
|
|
92
|
-
if (!a) return -1;
|
|
93
|
-
if (!b) return 1;
|
|
94
|
-
for (let index = 0; index < 3; index += 1) {
|
|
95
|
-
if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1;
|
|
96
|
-
}
|
|
97
|
-
return 0;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function extractSkillPath(args) {
|
|
101
|
-
let skillPath = null;
|
|
102
|
-
const forwardedArgs = [];
|
|
103
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
104
|
-
const arg = args[index];
|
|
105
|
-
if (arg === '--skill-path') {
|
|
106
|
-
if (!args[index + 1]) throw new Error('--skill-path 缺少 SKILL.md 路径');
|
|
107
|
-
skillPath = args[index + 1];
|
|
108
|
-
index += 1;
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (arg.startsWith('--skill-path=')) {
|
|
112
|
-
skillPath = arg.slice('--skill-path='.length);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
forwardedArgs.push(arg);
|
|
116
|
-
}
|
|
117
|
-
return { skillPath, forwardedArgs };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function isHelpRequest(args) {
|
|
121
|
-
return args.length === 0 || args.some((arg) => arg === '--help' || arg === '-h');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function printHelp() {
|
|
125
|
-
process.stdout.write(`${SKILL_DISPLAY_NAME} ${SKILL_VERSION}(Runtime ${RUNTIME_VERSION})
|
|
126
|
-
|
|
127
|
-
用法:
|
|
128
|
-
${SKILL_ID} --skill-path <SKILL.md> --start [扫描选项]
|
|
129
|
-
${SKILL_ID} --skill-path <SKILL.md> --poll-run <RUN_ID>
|
|
130
|
-
${SKILL_ID} --skill-path <SKILL.md> --run-status <RUN_ID>
|
|
131
|
-
${SKILL_ID} --skill-path <SKILL.md> --cancel-run <RUN_ID>
|
|
132
|
-
${SKILL_ID} --skill-path <SKILL.md> --first-response-scan [--since 24] [--limit 100]
|
|
58
|
+
/* ------------------------------------------------------------------ *
|
|
59
|
+
* 绑定层
|
|
60
|
+
*
|
|
61
|
+
* 这一层只做一件事:把上面那批「我是谁」的常量补给共享内核。里面不允许有判断逻辑,
|
|
62
|
+
* 有判断的部分(身份校验、退出协议、帮助文案)一律留在下面的本仓代码里。
|
|
63
|
+
*
|
|
64
|
+
* 保留同名薄封装而不是直接调用 core.*,是因为现有 index.test.js 是回归基线:
|
|
65
|
+
* 它按位置参数调用 pruneOldRuntimeVersions / reportPrunedRuntimeVersions,
|
|
66
|
+
* 按三参数调用 verifyReleaseArchive。封装在这里,测试一行都不用改。
|
|
67
|
+
* ------------------------------------------------------------------ */
|
|
68
|
+
|
|
69
|
+
const fail = core.createFailHandler({ logPrefix: SKILL_ID });
|
|
70
|
+
|
|
71
|
+
const emitStatus = core.createStatusEmitter({ skillVersion: SKILL_VERSION });
|
|
72
|
+
|
|
73
|
+
// 本仓把「不带任何参数」当作帮助请求(它没有可用的默认动作)。这个值必须显式写出来:
|
|
74
|
+
// public-customer-reactivation 的答案是相反的,内核因此不给默认值。
|
|
75
|
+
const isHelpRequest = (args) => core.isHelpRequest(args, { emptyArgsMeansHelp: true });
|
|
76
|
+
|
|
77
|
+
// 环境变量的**名字**属于本仓身份,留在这里;「显式覆盖主源就不追加默认备用源」那条规则在内核里。
|
|
78
|
+
// 必须在函数内部读 process.env:模块加载时读会让测试和联调无法在运行中改源。
|
|
79
|
+
const releaseBaseUrls = () =>
|
|
80
|
+
core.resolveReleaseBaseUrls({
|
|
81
|
+
defaultPrimary: OSS_RELEASE_BASE_URL,
|
|
82
|
+
defaultFallbacks: [R2_RELEASE_BASE_URL],
|
|
83
|
+
overridePrimary: process.env.PRIORITY_BUYER_ALERT_RELEASE_BASE_URL,
|
|
84
|
+
overrideFallbacks: process.env.PRIORITY_BUYER_ALERT_FALLBACK_BASE_URLS
|
|
85
|
+
});
|
|
133
86
|
|
|
134
|
-
|
|
135
|
-
--skill-path <PATH> 本次实际读取的 SKILL.md 绝对路径
|
|
136
|
-
--version 显示版本
|
|
137
|
-
--help 显示本帮助
|
|
87
|
+
const releaseUrls = (target) => core.releaseUrls(releaseBaseUrls(), target);
|
|
138
88
|
|
|
139
|
-
|
|
140
|
-
--minutes <N> 回复时限(SLA),默认 30
|
|
141
|
-
--lookback-hours <N> 只看最近 N 小时内有过消息的询盘,默认 24;0 表示不限
|
|
142
|
-
--max-pages <N> 翻页上限,每页 100 条,默认 3
|
|
143
|
-
--levels <LEVELS> 重点买家等级白名单,默认 L1+,L2,L3,L4 (阿里等级 L0-L4,L4 封顶)
|
|
144
|
-
--re-alert <N> 升级重提间隔;不给则只提醒一次
|
|
145
|
-
--assignee <NAME> 只看某个负责人
|
|
146
|
-
--no-strict-bot 关闭机器人核验
|
|
147
|
-
--verify-all-replied 对全部已回复的重点买家做详情核验
|
|
148
|
-
--mark 推进提醒水位;只有心跳式调用才应使用
|
|
89
|
+
const runtimeManifestUrls = () => core.runtimeManifestUrls(releaseBaseUrls());
|
|
149
90
|
|
|
150
|
-
|
|
151
|
-
--since <HOURS> 只看最近 N 小时内有消息的询盘,默认 24
|
|
152
|
-
--limit <N> 本轮最多核验多少条会话详情,默认 100
|
|
91
|
+
const runtimeManifestUrl = () => runtimeManifestUrls()[0];
|
|
153
92
|
|
|
154
|
-
|
|
155
|
-
|
|
93
|
+
// 清单 schema 是本 Skill 自己的字符串:两个 Skill 的产物可能放在同一个桶里,
|
|
94
|
+
// 只比版本号会让「下错 Skill 的包」一路通过校验。
|
|
95
|
+
const verifyReleaseArchive = (manifest, target, archivePath) =>
|
|
96
|
+
core.verifyReleaseArchive(manifest, target, archivePath, {
|
|
97
|
+
manifestSchema: RUNTIME_MANIFEST_SCHEMA,
|
|
98
|
+
runtimeVersion: RUNTIME_VERSION
|
|
99
|
+
});
|
|
156
100
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
101
|
+
// options 放在最后展开:测试会注入 baseUrls / downloadFile,注入值必须能盖掉默认值。
|
|
102
|
+
const downloadVerifiedRelease = (target, releaseManifestPath, archivePath, options = {}) =>
|
|
103
|
+
core.downloadVerifiedRelease(target, releaseManifestPath, archivePath, {
|
|
104
|
+
baseUrls: releaseBaseUrls(),
|
|
105
|
+
downloadFile: (url, outputPath) =>
|
|
106
|
+
core.download(url, outputPath, { userAgent: `@yixinkj/${SKILL_ID}-cli` }),
|
|
107
|
+
verifyArchive: verifyReleaseArchive,
|
|
108
|
+
...options
|
|
109
|
+
});
|
|
160
110
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
164
|
-
}
|
|
111
|
+
const detectTarget = () =>
|
|
112
|
+
core.detectTarget({ platforms: registry.platforms, binName: registry.skill.bin });
|
|
165
113
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const char = content[index];
|
|
174
|
-
const next = content[index + 1];
|
|
175
|
-
if (lineComment) {
|
|
176
|
-
if (char === '\n') {
|
|
177
|
-
lineComment = false;
|
|
178
|
-
output += char;
|
|
179
|
-
}
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
if (blockComment) {
|
|
183
|
-
if (char === '*' && next === '/') {
|
|
184
|
-
blockComment = false;
|
|
185
|
-
index += 1;
|
|
186
|
-
}
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
if (inString) {
|
|
190
|
-
output += char;
|
|
191
|
-
if (escaped) escaped = false;
|
|
192
|
-
else if (char === '\\') escaped = true;
|
|
193
|
-
else if (char === '"') inString = false;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (char === '"') {
|
|
197
|
-
inString = true;
|
|
198
|
-
output += char;
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (char === '/' && next === '/') {
|
|
202
|
-
lineComment = true;
|
|
203
|
-
index += 1;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (char === '/' && next === '*') {
|
|
207
|
-
blockComment = true;
|
|
208
|
-
index += 1;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
output += char;
|
|
212
|
-
}
|
|
213
|
-
return output.replace(/,\s*([}\]])/g, '$1');
|
|
214
|
-
}
|
|
114
|
+
const isInstalled = (target) =>
|
|
115
|
+
core.isRuntimeInstalled({
|
|
116
|
+
target,
|
|
117
|
+
binDir: BIN_DIR,
|
|
118
|
+
installMarkerPath: INSTALL_MARKER,
|
|
119
|
+
runtimeVersion: RUNTIME_VERSION
|
|
120
|
+
});
|
|
215
121
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
122
|
+
// 位置参数原样保留,index.test.js 里 `pruneOldRuntimeVersions(root, 'v0.1.1', 2)` 不用改。
|
|
123
|
+
const pruneOldRuntimeVersions = (
|
|
124
|
+
versionsRoot = path.dirname(VERSION_ROOT),
|
|
125
|
+
active = path.basename(VERSION_ROOT),
|
|
126
|
+
keepCount = core.RUNTIME_VERSIONS_TO_KEEP
|
|
127
|
+
) => core.pruneOldRuntimeVersions({ versionsRoot, active, keepCount });
|
|
128
|
+
|
|
129
|
+
// logPrefix 必填就是那次事故的正解:漏传时在入口报「缺少 logPrefix」,
|
|
130
|
+
// 而不是等到真的删掉了旧版本、走到打印那一行才崩掉整个启动。
|
|
131
|
+
// 这里刻意不传 log:内核的默认值是在每次调用时取 console.error,
|
|
132
|
+
// 测试替换 console.error 才能截到这行日志。
|
|
133
|
+
const reportPrunedRuntimeVersions = (
|
|
134
|
+
versionsRoot = path.dirname(VERSION_ROOT),
|
|
135
|
+
active = path.basename(VERSION_ROOT),
|
|
136
|
+
keepCount = core.RUNTIME_VERSIONS_TO_KEEP
|
|
137
|
+
) => core.reportPrunedRuntimeVersions({ versionsRoot, active, keepCount, logPrefix: SKILL_ID });
|
|
138
|
+
|
|
139
|
+
const ensureInstalled = (target) =>
|
|
140
|
+
core.ensureRuntimeInstalled({
|
|
141
|
+
target,
|
|
142
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
143
|
+
versionRoot: VERSION_ROOT,
|
|
144
|
+
binDir: BIN_DIR,
|
|
145
|
+
tempDirPrefix: `${SKILL_ID}-install-`,
|
|
146
|
+
logPrefix: SKILL_ID,
|
|
147
|
+
downloadRelease: downloadVerifiedRelease
|
|
148
|
+
});
|
|
219
149
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
(
|
|
224
|
-
|
|
225
|
-
|
|
150
|
+
// 桥的 import 由这里注入,内核因此不静态依赖任何第三方包。
|
|
151
|
+
const ensureSharedBridge = () =>
|
|
152
|
+
core.ensureSharedBridge({
|
|
153
|
+
importBridge: () => import('@yixinkj/yixin-bridge-cli'),
|
|
154
|
+
minVersion: registry.bridge.minVersion,
|
|
155
|
+
requiredCapabilities: registry.bridge.requiredCapabilities || []
|
|
156
|
+
});
|
|
226
157
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const registryPath = path.join(skillDir, '..', 'skills.jsonc');
|
|
230
|
-
if (!fs.existsSync(registryPath)) {
|
|
231
|
-
return { registryPath: null, currentVersion: null, changed: false, content: null };
|
|
232
|
-
}
|
|
233
|
-
const data = readSkillsRegistry(registryPath);
|
|
234
|
-
const entry = findRegistryEntry(data, skillDir);
|
|
235
|
-
if (!entry) {
|
|
236
|
-
throw new Error(`skills.jsonc 未找到 installPath 对应项:${skillDir}`);
|
|
237
|
-
}
|
|
238
|
-
const before = JSON.stringify(entry);
|
|
239
|
-
const currentVersion = entry.version || null;
|
|
240
|
-
entry.name = metadata.name;
|
|
241
|
-
entry.description = metadata.description;
|
|
242
|
-
entry.version = SKILL_VERSION;
|
|
243
|
-
return {
|
|
244
|
-
registryPath,
|
|
245
|
-
currentVersion,
|
|
246
|
-
changed: before !== JSON.stringify(entry),
|
|
247
|
-
content: `${JSON.stringify(data, null, 2)}\n`
|
|
248
|
-
};
|
|
249
|
-
}
|
|
158
|
+
const emitBrowserExtensionNotice = (sharedBridge) =>
|
|
159
|
+
core.emitBrowserExtensionNotice(sharedBridge, { logPrefix: SKILL_ID });
|
|
250
160
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
161
|
+
// 识别特征(包名出现在正文里,或 frontmatter 的 name 就是内部 ID)属于本仓。
|
|
162
|
+
const discoverLegacySkillPaths = (homeDir = os.homedir()) =>
|
|
163
|
+
core.discoverLegacySkillPaths({
|
|
164
|
+
homeDir,
|
|
165
|
+
matches: (content) =>
|
|
166
|
+
content.includes(`@yixinkj/${SKILL_ID}-cli`) ||
|
|
167
|
+
new RegExp(`^name:\\s*${SKILL_ID}\\s*$`, 'm').test(content)
|
|
168
|
+
});
|
|
256
169
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
170
|
+
// 'require_argument' 是本仓现有语义:没传 --skill-path 就永远不放行,并在结论里点名参数和路径。
|
|
171
|
+
// 缺的是参数,不是同步;含糊的「请重新试一次」正好会诱导调用方原样重试——重试一万次也不会成功。
|
|
172
|
+
// 内核把这个参数设成必填、不给默认值,就是为了不让它在迁移里被顺手改成另外两仓的
|
|
173
|
+
// 'restart_required'(那会连带改掉退出协议)。
|
|
174
|
+
const resolveSkillGate = (skillPath, { homeDir = os.homedir(), syncOptions = {} } = {}) =>
|
|
175
|
+
core.resolveSkillGate(skillPath, {
|
|
176
|
+
syncSkill: (found) => syncSkillInstallation(found, syncOptions),
|
|
177
|
+
discoverSkillPaths: () => discoverLegacySkillPaths(homeDir),
|
|
178
|
+
discoveredPathPolicy: 'require_argument'
|
|
179
|
+
});
|
|
262
180
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
267
|
-
return marker.version || registryVersion || null;
|
|
268
|
-
} catch {
|
|
269
|
-
return registryVersion || null;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
181
|
+
/* ------------------------------------------------------------------ *
|
|
182
|
+
* 本仓自己的逻辑
|
|
183
|
+
* ------------------------------------------------------------------ */
|
|
272
184
|
|
|
273
185
|
/**
|
|
274
186
|
* 把 npm 包内置的 SKILL.md 同步到当前智能体实际使用的安装位置。
|
|
275
187
|
* 本地版本更高时拒绝降级;任一校验失败时整体回滚,不留下半个新版 Skill。
|
|
188
|
+
*
|
|
189
|
+
* 这个函数没有被提取进共享内核:三个仓真正不同的地方就在这里——同步的产物不同
|
|
190
|
+
* (本仓只有 SKILL.md + 版本标记)、身份判定不同(本仓认展示名、内部 ID 和历史展示名白名单)、
|
|
191
|
+
* 版本来源不同。做成一个带三套插件点的通用函数只会变成隐式行为的温床。
|
|
192
|
+
* 内部真正共用且最危险的四块(路径校验、内置包校验、registry 预演/回读、可回滚事务)走内核。
|
|
276
193
|
* @param {string} skillPath 本次实际读取的 SKILL.md 绝对路径。
|
|
277
194
|
* @param {object} [options] 测试注入的内置文件路径。
|
|
278
195
|
* @returns {object} 同步结论与版本信息。
|
|
279
196
|
*/
|
|
280
197
|
function syncSkillInstallation(skillPath, { bundledSkillPath = BUNDLED_SKILL_PATH } = {}) {
|
|
281
|
-
const resolvedSkillPath =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
if (!fs.existsSync(resolvedSkillPath)) {
|
|
286
|
-
throw new Error(`SKILL.md 不存在:${resolvedSkillPath}`);
|
|
287
|
-
}
|
|
288
|
-
if (!fs.existsSync(bundledSkillPath)) {
|
|
289
|
-
throw new Error('npm 包缺少内置 SKILL.md,请重新安装当前版本');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const bundledSkill = fs.readFileSync(bundledSkillPath, 'utf8');
|
|
293
|
-
const bundledMetadata = parseSkillMetadata(bundledSkill);
|
|
294
|
-
if (!bundledMetadata.name || !bundledMetadata.description) {
|
|
295
|
-
throw new Error('内置 SKILL.md 缺少 name 或 description');
|
|
296
|
-
}
|
|
198
|
+
const resolvedSkillPath = core.resolveSkillMdPath(skillPath);
|
|
199
|
+
const { content: bundledSkill, metadata: bundledMetadata } = core.loadBundledSkill(bundledSkillPath);
|
|
200
|
+
// 内核只检查包自身完整性(文件在不在、name/description 有没有),身份判定留在本仓。
|
|
297
201
|
if (bundledMetadata.name !== SKILL_DISPLAY_NAME) {
|
|
298
202
|
throw new Error('内置 SKILL.md 名称不匹配');
|
|
299
203
|
}
|
|
300
204
|
|
|
301
205
|
const currentSkill = fs.readFileSync(resolvedSkillPath, 'utf8');
|
|
302
|
-
const currentSkillName = parseSkillMetadata(currentSkill).name;
|
|
206
|
+
const currentSkillName = core.parseSkillMetadata(currentSkill).name;
|
|
303
207
|
// 旧版 frontmatter 使用内部 ID 作为 name;允许它被新展示名平滑升级,避免 OTA 被自身名称校验挡住。
|
|
304
208
|
if (
|
|
305
209
|
currentSkillName !== SKILL_ID &&
|
|
@@ -310,9 +214,14 @@ function syncSkillInstallation(skillPath, { bundledSkillPath = BUNDLED_SKILL_PAT
|
|
|
310
214
|
}
|
|
311
215
|
|
|
312
216
|
const skillDir = path.dirname(resolvedSkillPath);
|
|
313
|
-
const registryPreview = registryUpdatePreview(resolvedSkillPath, bundledMetadata
|
|
314
|
-
|
|
315
|
-
|
|
217
|
+
const registryPreview = core.registryUpdatePreview(resolvedSkillPath, bundledMetadata, {
|
|
218
|
+
targetVersion: SKILL_VERSION
|
|
219
|
+
});
|
|
220
|
+
const currentVersion = core.readInstalledSkillVersion(skillDir, {
|
|
221
|
+
markerFileName: SKILL_VERSION_MARKER,
|
|
222
|
+
registryVersion: registryPreview.currentVersion
|
|
223
|
+
});
|
|
224
|
+
if (core.compareVersions(currentVersion, SKILL_VERSION) > 0) {
|
|
316
225
|
return {
|
|
317
226
|
action: 'local_newer',
|
|
318
227
|
skillPath: resolvedSkillPath,
|
|
@@ -322,7 +231,8 @@ function syncSkillInstallation(skillPath, { bundledSkillPath = BUNDLED_SKILL_PAT
|
|
|
322
231
|
}
|
|
323
232
|
|
|
324
233
|
const markerPath = path.join(skillDir, SKILL_VERSION_MARKER);
|
|
325
|
-
|
|
234
|
+
// 写入内容和比对内容必须来自同一个函数,否则「写时带换行、比时不带」会让每次启动都判定有变化。
|
|
235
|
+
const markerContent = core.versionMarkerContent(SKILL_VERSION);
|
|
326
236
|
const skillChanged = Buffer.from(currentSkill).compare(Buffer.from(bundledSkill)) !== 0;
|
|
327
237
|
const markerChanged =
|
|
328
238
|
!fs.existsSync(markerPath) || fs.readFileSync(markerPath, 'utf8') !== markerContent;
|
|
@@ -337,44 +247,39 @@ function syncSkillInstallation(skillPath, { bundledSkillPath = BUNDLED_SKILL_PAT
|
|
|
337
247
|
};
|
|
338
248
|
}
|
|
339
249
|
|
|
340
|
-
|
|
341
|
-
[
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
) {
|
|
371
|
-
throw new Error('写入后的 skills.jsonc 校验失败');
|
|
250
|
+
core.runWithRollback({
|
|
251
|
+
files: [
|
|
252
|
+
resolvedSkillPath,
|
|
253
|
+
markerPath,
|
|
254
|
+
...(registryPreview.registryPath ? [registryPreview.registryPath] : [])
|
|
255
|
+
],
|
|
256
|
+
apply: () => {
|
|
257
|
+
if (skillChanged) fs.writeFileSync(resolvedSkillPath, bundledSkill);
|
|
258
|
+
if (markerChanged) fs.writeFileSync(markerPath, markerContent);
|
|
259
|
+
if (registryPreview.changed) {
|
|
260
|
+
fs.writeFileSync(registryPreview.registryPath, registryPreview.content);
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
// 自检必须在事务内:校验失败同样要整体回滚,否则「校验」只是打印了一句话。
|
|
264
|
+
verify: () => {
|
|
265
|
+
const writtenMetadata = core.parseSkillMetadata(fs.readFileSync(resolvedSkillPath, 'utf8'));
|
|
266
|
+
if (writtenMetadata.name !== bundledMetadata.name) {
|
|
267
|
+
throw new Error('写入后的 SKILL.md 名称校验失败');
|
|
268
|
+
}
|
|
269
|
+
if (JSON.parse(fs.readFileSync(markerPath, 'utf8')).version !== SKILL_VERSION) {
|
|
270
|
+
throw new Error('写入后的 Skill 版本标记校验失败');
|
|
271
|
+
}
|
|
272
|
+
if (registryPreview.registryPath) {
|
|
273
|
+
core.assertRegistryEntryWritten({
|
|
274
|
+
registryPath: registryPreview.registryPath,
|
|
275
|
+
skillDir,
|
|
276
|
+
version: SKILL_VERSION,
|
|
277
|
+
name: bundledMetadata.name,
|
|
278
|
+
description: bundledMetadata.description
|
|
279
|
+
});
|
|
372
280
|
}
|
|
373
281
|
}
|
|
374
|
-
}
|
|
375
|
-
for (const [filePath, snapshot] of snapshots) restoreFile(filePath, snapshot);
|
|
376
|
-
throw error;
|
|
377
|
-
}
|
|
282
|
+
});
|
|
378
283
|
|
|
379
284
|
return {
|
|
380
285
|
action: skillChanged ? 'updated' : 'registry_updated',
|
|
@@ -385,513 +290,58 @@ function syncSkillInstallation(skillPath, { bundledSkillPath = BUNDLED_SKILL_PAT
|
|
|
385
290
|
};
|
|
386
291
|
}
|
|
387
292
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const candidates = [];
|
|
392
|
-
for (const account of fs.readdirSync(accountsRoot, { withFileTypes: true })) {
|
|
393
|
-
if (!account.isDirectory()) continue;
|
|
394
|
-
const agentsRoot = path.join(accountsRoot, account.name, 'agents');
|
|
395
|
-
if (!fs.existsSync(agentsRoot)) continue;
|
|
396
|
-
for (const agent of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
397
|
-
if (!agent.isDirectory()) continue;
|
|
398
|
-
const registryPath = path.join(
|
|
399
|
-
agentsRoot,
|
|
400
|
-
agent.name,
|
|
401
|
-
'agent-core',
|
|
402
|
-
'skills',
|
|
403
|
-
'skills.jsonc'
|
|
404
|
-
);
|
|
405
|
-
if (!fs.existsSync(registryPath)) continue;
|
|
406
|
-
let data;
|
|
407
|
-
try {
|
|
408
|
-
data = readSkillsRegistry(registryPath);
|
|
409
|
-
} catch {
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
for (const entry of data.skills || []) {
|
|
413
|
-
if (!entry.installPath) continue;
|
|
414
|
-
const skillPath = path.join(entry.installPath, 'SKILL.md');
|
|
415
|
-
if (!fs.existsSync(skillPath)) continue;
|
|
416
|
-
let content;
|
|
417
|
-
try {
|
|
418
|
-
content = fs.readFileSync(skillPath, 'utf8');
|
|
419
|
-
} catch {
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
if (
|
|
423
|
-
content.includes(`@yixinkj/${SKILL_ID}-cli`) ||
|
|
424
|
-
new RegExp(`^name:\\s*${SKILL_ID}\\s*$`, 'm').test(content)
|
|
425
|
-
) {
|
|
426
|
-
candidates.push(path.resolve(skillPath));
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return [...new Set(candidates.map(normalizePath))];
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function resolveSkillGate(skillPath, { homeDir = os.homedir(), syncOptions = {} } = {}) {
|
|
435
|
-
if (skillPath) {
|
|
436
|
-
const sync = syncSkillInstallation(skillPath, syncOptions);
|
|
437
|
-
return { allowRun: sync.action === 'current', sync };
|
|
438
|
-
}
|
|
439
|
-
const candidates = discoverLegacySkillPaths(homeDir);
|
|
440
|
-
if (candidates.length !== 1) {
|
|
441
|
-
return { allowRun: false, sync: { action: 'skill_path_required', candidates } };
|
|
442
|
-
}
|
|
443
|
-
const sync = syncSkillInstallation(candidates[0], syncOptions);
|
|
444
|
-
if (sync.action === 'local_newer') {
|
|
445
|
-
return { allowRun: false, sync };
|
|
446
|
-
}
|
|
447
|
-
// 走到这里说明调用方没传 --skill-path,是靠扫描猜出来的安装位置。这条路径永远不放行:
|
|
448
|
-
// 缺的是参数,不是同步。提示必须点名参数和路径,否则调用方只会原样重试——重试一万次
|
|
449
|
-
// 也不会成功,而含糊的「请重新试一次」正好会诱导它这么做。
|
|
450
|
-
return {
|
|
451
|
-
allowRun: false,
|
|
452
|
-
sync: { ...sync, action: 'skill_path_required', candidates, synced: sync.action !== 'current' }
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function detectTarget() {
|
|
457
|
-
const platform = registry.platforms[`${process.platform}-${process.arch}`];
|
|
458
|
-
if (!platform) {
|
|
459
|
-
const supported = Object.values(registry.platforms)
|
|
460
|
-
.map((entry) => entry.id)
|
|
461
|
-
.join('、');
|
|
462
|
-
throw new Error(
|
|
463
|
-
`暂不支持当前平台:${process.platform}/${process.arch}。已支持:${supported}。`
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
return { ...platform, binaries: [`${registry.skill.bin}-${platform.id}${platform.exe || ''}`] };
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* 生成业务运行时下载源,固定保持 OSS 主源优先、R2 备用源在后。
|
|
471
|
-
* @returns {string[]} 已清理、去重并保持优先级的下载根地址。
|
|
472
|
-
*/
|
|
473
|
-
function releaseBaseUrls() {
|
|
474
|
-
const primary = process.env.PRIORITY_BUYER_ALERT_RELEASE_BASE_URL || OSS_RELEASE_BASE_URL;
|
|
475
|
-
const fallbacks = process.env.PRIORITY_BUYER_ALERT_FALLBACK_BASE_URLS
|
|
476
|
-
? process.env.PRIORITY_BUYER_ALERT_FALLBACK_BASE_URLS.split(',')
|
|
477
|
-
: process.env.PRIORITY_BUYER_ALERT_RELEASE_BASE_URL
|
|
478
|
-
? []
|
|
479
|
-
: [R2_RELEASE_BASE_URL];
|
|
480
|
-
return [
|
|
481
|
-
...new Set(
|
|
482
|
-
[primary, ...fallbacks]
|
|
483
|
-
.filter(Boolean)
|
|
484
|
-
.map((value) => String(value).trim().replace(/\/$/, ''))
|
|
485
|
-
)
|
|
486
|
-
];
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function releaseUrls(target) {
|
|
490
|
-
return releaseBaseUrls().map((baseUrl) => `${baseUrl}/${target.archive}`);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function runtimeManifestUrls() {
|
|
494
|
-
return releaseBaseUrls().map((baseUrl) => `${baseUrl}/runtime-manifest.json`);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function runtimeManifestUrl() {
|
|
498
|
-
return runtimeManifestUrls()[0];
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function sha256(filePath) {
|
|
502
|
-
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function isInstalled(target) {
|
|
506
|
-
try {
|
|
507
|
-
const marker = JSON.parse(fs.readFileSync(INSTALL_MARKER, 'utf8'));
|
|
508
|
-
return (
|
|
509
|
-
marker.version === RUNTIME_VERSION &&
|
|
510
|
-
marker.platform === target.id &&
|
|
511
|
-
target.binaries.every((name) => {
|
|
512
|
-
const expected = marker.files?.[name];
|
|
513
|
-
const filePath = path.join(BIN_DIR, name);
|
|
514
|
-
return (
|
|
515
|
-
expected &&
|
|
516
|
-
fs.existsSync(filePath) &&
|
|
517
|
-
fs.statSync(filePath).size === expected.size &&
|
|
518
|
-
sha256(filePath) === expected.sha256
|
|
519
|
-
);
|
|
520
|
-
})
|
|
521
|
-
);
|
|
522
|
-
} catch {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function download(url, outputPath, redirects = 5) {
|
|
528
|
-
return new Promise((resolve, reject) => {
|
|
529
|
-
const client = url.startsWith('http:') ? http : https;
|
|
530
|
-
const request = client.get(
|
|
531
|
-
url,
|
|
532
|
-
{ headers: { 'User-Agent': `@yixinkj/${SKILL_ID}-cli` } },
|
|
533
|
-
(response) => {
|
|
534
|
-
if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
|
|
535
|
-
response.resume();
|
|
536
|
-
if (!response.headers.location || redirects === 0) {
|
|
537
|
-
reject(new Error(`下载重定向过多:${url}`));
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
download(
|
|
541
|
-
new URL(response.headers.location, url).toString(),
|
|
542
|
-
outputPath,
|
|
543
|
-
redirects - 1
|
|
544
|
-
).then(resolve, reject);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (response.statusCode !== 200) {
|
|
548
|
-
response.resume();
|
|
549
|
-
reject(new Error(`下载失败:HTTP ${response.statusCode} ${url}`));
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
const file = fs.createWriteStream(outputPath);
|
|
553
|
-
response.pipe(file);
|
|
554
|
-
response.on('aborted', () => {
|
|
555
|
-
file.destroy();
|
|
556
|
-
reject(new Error(`下载连接意外中断:${url}`));
|
|
557
|
-
});
|
|
558
|
-
response.on('error', (error) => file.destroy(error));
|
|
559
|
-
file.on('finish', () => file.close(resolve));
|
|
560
|
-
file.on('error', reject);
|
|
561
|
-
}
|
|
562
|
-
);
|
|
563
|
-
request.setTimeout(60000, () => request.destroy(new Error(`下载超时:${url}`)));
|
|
564
|
-
request.on('error', reject);
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* 从同一个来源成对下载清单和压缩包,并在切换来源前完成版本、平台和 SHA256 校验。
|
|
570
|
-
* 清单与文件必须同源处理,避免主源清单与备用源文件版本漂移时被错误组合。
|
|
571
|
-
* @param {object} target 当前平台的归档名称和标识。
|
|
572
|
-
* @param {string} releaseManifestPath 临时清单文件路径。
|
|
573
|
-
* @param {string} archivePath 临时运行时归档路径。
|
|
574
|
-
* @param {object} [options] 下载源和测试注入选项。
|
|
575
|
-
* @returns {Promise<object>} 与归档同源且已通过校验的运行时清单。
|
|
576
|
-
*/
|
|
577
|
-
async function downloadVerifiedRelease(
|
|
578
|
-
target,
|
|
579
|
-
releaseManifestPath,
|
|
580
|
-
archivePath,
|
|
581
|
-
{ baseUrls = releaseBaseUrls(), downloadFile = download } = {}
|
|
582
|
-
) {
|
|
583
|
-
const failures = [];
|
|
584
|
-
for (const baseUrl of baseUrls) {
|
|
585
|
-
const root = String(baseUrl).trim().replace(/\/$/, '');
|
|
586
|
-
try {
|
|
587
|
-
fs.rmSync(releaseManifestPath, { force: true });
|
|
588
|
-
fs.rmSync(archivePath, { force: true });
|
|
589
|
-
await downloadFile(`${root}/runtime-manifest.json`, releaseManifestPath);
|
|
590
|
-
const manifest = JSON.parse(fs.readFileSync(releaseManifestPath, 'utf8'));
|
|
591
|
-
await downloadFile(`${root}/${target.archive}`, archivePath);
|
|
592
|
-
verifyReleaseArchive(manifest, target, archivePath);
|
|
593
|
-
return manifest;
|
|
594
|
-
} catch (error) {
|
|
595
|
-
fs.rmSync(releaseManifestPath, { force: true });
|
|
596
|
-
fs.rmSync(archivePath, { force: true });
|
|
597
|
-
failures.push(`${root}: ${error.message}`);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
throw new Error(`所有下载源均未通过运行时校验:\n${failures.join('\n')}`);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function verifyReleaseArchive(manifest, target, archivePath) {
|
|
604
|
-
if (
|
|
605
|
-
manifest?.schema !== RUNTIME_MANIFEST_SCHEMA ||
|
|
606
|
-
manifest.runtime_version !== RUNTIME_VERSION
|
|
607
|
-
) {
|
|
608
|
-
throw new Error('公共运行时清单版本与当前启动器不一致');
|
|
609
|
-
}
|
|
610
|
-
const expected = manifest.platforms?.[target.id];
|
|
611
|
-
if (!expected || expected.archive !== target.archive) {
|
|
612
|
-
throw new Error(`公共运行时清单缺少平台 ${target.id}`);
|
|
613
|
-
}
|
|
614
|
-
const stat = fs.statSync(archivePath);
|
|
615
|
-
if (stat.size !== expected.size || sha256(archivePath) !== expected.sha256) {
|
|
616
|
-
throw new Error(`公共运行时压缩包校验失败:${target.archive}`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
293
|
+
/** 帮助文案是本仓的命令面,不进共享内核。 */
|
|
294
|
+
function printHelp() {
|
|
295
|
+
process.stdout.write(`${SKILL_DISPLAY_NAME} ${SKILL_VERSION}(Runtime ${RUNTIME_VERSION})
|
|
619
296
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
626
|
-
if (result.error) throw result.error;
|
|
627
|
-
if (result.status !== 0) {
|
|
628
|
-
const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
629
|
-
throw new Error(detail || `${command} 退出码为 ${result.status}`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
297
|
+
用法:
|
|
298
|
+
${SKILL_ID} --skill-path <SKILL.md> --start [扫描选项]
|
|
299
|
+
${SKILL_ID} --skill-path <SKILL.md> --poll-run <RUN_ID>
|
|
300
|
+
${SKILL_ID} --skill-path <SKILL.md> --run-status <RUN_ID>
|
|
301
|
+
${SKILL_ID} --skill-path <SKILL.md> --cancel-run <RUN_ID>
|
|
302
|
+
${SKILL_ID} --skill-path <SKILL.md> --first-response-scan [--since 24] [--limit 100]
|
|
632
303
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
if (archivePath.endsWith('.zip')) {
|
|
640
|
-
if (process.platform === 'win32') {
|
|
641
|
-
runCommand('powershell.exe', [
|
|
642
|
-
'-NoProfile',
|
|
643
|
-
'-ExecutionPolicy',
|
|
644
|
-
'Bypass',
|
|
645
|
-
'-Command',
|
|
646
|
-
`Expand-Archive -LiteralPath ${JSON.stringify(archivePath)} -DestinationPath ${JSON.stringify(destination)} -Force`
|
|
647
|
-
]);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
runCommand('unzip', ['-q', archivePath, '-d', destination]);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
throw new Error(`暂不支持的压缩包类型:${archivePath}`);
|
|
654
|
-
}
|
|
304
|
+
通用:
|
|
305
|
+
--skill-path <PATH> 本次实际读取的 SKILL.md 绝对路径
|
|
306
|
+
--version 显示版本
|
|
307
|
+
--help 显示本帮助
|
|
655
308
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (
|
|
667
|
-
!expected ||
|
|
668
|
-
!fs.existsSync(filePath) ||
|
|
669
|
-
fs.statSync(filePath).size !== expected.size ||
|
|
670
|
-
sha256(filePath) !== expected.sha256
|
|
671
|
-
) {
|
|
672
|
-
throw new Error(`下载包校验失败:${name}`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return manifest.files;
|
|
676
|
-
}
|
|
309
|
+
扫描选项(仅与 --start 一起使用):
|
|
310
|
+
--minutes <N> 回复时限(SLA),默认 30
|
|
311
|
+
--lookback-hours <N> 只看最近 N 小时内有过消息的询盘,默认 24;0 表示不限
|
|
312
|
+
--max-pages <N> 翻页上限,每页 100 条,默认 3
|
|
313
|
+
--levels <LEVELS> 重点买家等级白名单,默认 L1+,L2,L3,L4 (阿里等级 L0-L4,L4 封顶)
|
|
314
|
+
--re-alert <N> 升级重提间隔;不给则只提醒一次
|
|
315
|
+
--assignee <NAME> 只看某个负责人
|
|
316
|
+
--no-strict-bot 关闭机器人核验
|
|
317
|
+
--verify-all-replied 对全部已回复的重点买家做详情核验
|
|
318
|
+
--mark 推进提醒水位;只有心跳式调用才应使用
|
|
677
319
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const filePath = path.join(BIN_DIR, name);
|
|
682
|
-
spawnSync('xattr', ['-dr', 'com.apple.quarantine', filePath], { stdio: 'ignore' });
|
|
683
|
-
runCommand('chmod', ['+x', filePath]);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
320
|
+
首响扫描选项(仅与 --first-response-scan 一起使用):
|
|
321
|
+
--since <HOURS> 只看最近 N 小时内有消息的询盘,默认 24
|
|
322
|
+
--limit <N> 本轮最多核验多少条会话详情,默认 100
|
|
686
323
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
/// 每升一版就多留一份二进制,从来没人删。真机上 public-customer-reactivation 攒了 16 份
|
|
690
|
-
/// 共 94M,三个 Skill 合计 164M,而任何时刻真正被执行的只有一份。
|
|
691
|
-
/// 不设成 1 是因为同一台机器会交替跑不同 dist-tag(例如 latest 与 beta),
|
|
692
|
-
/// 只留当前版会让每次切换都重新下载一遍。
|
|
693
|
-
const RUNTIME_VERSIONS_TO_KEEP = 2;
|
|
324
|
+
环境变量:
|
|
325
|
+
PRIORITY_BUYER_ALERT_DETAIL_CONCURRENCY 详情核验并发度,默认 3,范围 1-8
|
|
694
326
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
*
|
|
698
|
-
* 尽力而为:删不掉就算了,绝不因为清理失败影响本次运行。
|
|
699
|
-
* @returns {string[]} 实际删掉的版本目录名。
|
|
700
|
-
*/
|
|
701
|
-
function pruneOldRuntimeVersions(
|
|
702
|
-
versionsRoot = path.dirname(VERSION_ROOT),
|
|
703
|
-
active = path.basename(VERSION_ROOT),
|
|
704
|
-
keepCount = RUNTIME_VERSIONS_TO_KEEP
|
|
705
|
-
) {
|
|
706
|
-
let entries;
|
|
707
|
-
try {
|
|
708
|
-
entries = fs.readdirSync(versionsRoot, { withFileTypes: true });
|
|
709
|
-
} catch {
|
|
710
|
-
return [];
|
|
711
|
-
}
|
|
712
|
-
// 先把当前版本的时间戳刷新成现在:保留依据是「最近用过」而不是「最近装的」,
|
|
713
|
-
// 否则交替使用两个版本时,先装的那个会被误判成最旧的。
|
|
714
|
-
try {
|
|
715
|
-
const now = new Date();
|
|
716
|
-
fs.utimesSync(path.join(versionsRoot, active), now, now);
|
|
717
|
-
} catch {
|
|
718
|
-
// 目录还没建好(首次安装前)就跳过,不影响后面的判断。
|
|
719
|
-
}
|
|
720
|
-
const ranked = entries
|
|
721
|
-
.filter((entry) => entry.isDirectory())
|
|
722
|
-
.map((entry) => entry.name)
|
|
723
|
-
// 排除 v<版本>.staging-<pid>:那可能是另一个进程正在写的半成品。
|
|
724
|
-
.filter((name) => /^v\d/.test(name) && !name.includes('.staging-'))
|
|
725
|
-
.map((name) => {
|
|
726
|
-
let usedAt = 0;
|
|
727
|
-
try {
|
|
728
|
-
usedAt = fs.statSync(path.join(versionsRoot, name)).mtimeMs;
|
|
729
|
-
} catch {
|
|
730
|
-
usedAt = 0;
|
|
731
|
-
}
|
|
732
|
-
return { name, usedAt };
|
|
733
|
-
})
|
|
734
|
-
.sort((a, b) => b.usedAt - a.usedAt);
|
|
735
|
-
|
|
736
|
-
const keep = new Set(ranked.slice(0, keepCount).map((item) => item.name));
|
|
737
|
-
keep.add(active);
|
|
738
|
-
|
|
739
|
-
const removed = [];
|
|
740
|
-
for (const { name } of ranked) {
|
|
741
|
-
if (keep.has(name)) continue;
|
|
742
|
-
try {
|
|
743
|
-
fs.rmSync(path.join(versionsRoot, name), { recursive: true, force: true });
|
|
744
|
-
removed.push(name);
|
|
745
|
-
} catch {
|
|
746
|
-
// 正被占用或权限不足都不是本次运行该处理的问题,留到下次。
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return removed;
|
|
327
|
+
本 Skill 只读:只提醒,不发送消息、不修改任何页面状态。
|
|
328
|
+
`);
|
|
750
329
|
}
|
|
751
330
|
|
|
752
331
|
/**
|
|
753
|
-
*
|
|
332
|
+
* 启动流程。
|
|
754
333
|
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
334
|
+
* 这段也没有进共享内核:三个仓的退出协议不同(本仓在 skill_path_required 时置
|
|
335
|
+
* exitCode = 1,另外两仓直接 return),文案里的动作名也不同。把它模板化会让「改一句文案」
|
|
336
|
+
* 变成「改共享库」,更糟的是会悄悄统一退出码——退出码是调用方(智能体)的协议。
|
|
757
337
|
*/
|
|
758
|
-
function reportPrunedRuntimeVersions(...args) {
|
|
759
|
-
const removed = pruneOldRuntimeVersions(...args);
|
|
760
|
-
if (removed.length > 0) {
|
|
761
|
-
console.error(`[${SKILL_ID}] 已清理旧版本运行时:${removed.join('、')}`);
|
|
762
|
-
}
|
|
763
|
-
return removed;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
async function ensureInstalled(target) {
|
|
767
|
-
if (isInstalled(target)) {
|
|
768
|
-
prepareMacBinaries(target);
|
|
769
|
-
reportPrunedRuntimeVersions();
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `${SKILL_ID}-install-`));
|
|
773
|
-
const archivePath = path.join(tempRoot, target.archive);
|
|
774
|
-
const releaseManifestPath = path.join(tempRoot, 'runtime-manifest.json');
|
|
775
|
-
const extractedDir = path.join(tempRoot, 'extracted');
|
|
776
|
-
const stagingRoot = `${VERSION_ROOT}.staging-${process.pid}`;
|
|
777
|
-
try {
|
|
778
|
-
await downloadVerifiedRelease(target, releaseManifestPath, archivePath);
|
|
779
|
-
extractArchive(archivePath, extractedDir);
|
|
780
|
-
const files = verifyExtracted(extractedDir, target);
|
|
781
|
-
|
|
782
|
-
fs.mkdirSync(path.dirname(VERSION_ROOT), { recursive: true });
|
|
783
|
-
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
784
|
-
fs.mkdirSync(path.join(stagingRoot, 'bin'), { recursive: true });
|
|
785
|
-
for (const name of target.binaries) {
|
|
786
|
-
fs.copyFileSync(path.join(extractedDir, name), path.join(stagingRoot, 'bin', name));
|
|
787
|
-
}
|
|
788
|
-
fs.writeFileSync(
|
|
789
|
-
path.join(stagingRoot, 'installed.json'),
|
|
790
|
-
`${JSON.stringify({ version: RUNTIME_VERSION, platform: target.id, files }, null, 2)}\n`
|
|
791
|
-
);
|
|
792
|
-
|
|
793
|
-
fs.rmSync(VERSION_ROOT, { recursive: true, force: true });
|
|
794
|
-
fs.renameSync(stagingRoot, VERSION_ROOT);
|
|
795
|
-
prepareMacBinaries(target);
|
|
796
|
-
reportPrunedRuntimeVersions();
|
|
797
|
-
} finally {
|
|
798
|
-
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
799
|
-
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
async function ensureSharedBridge() {
|
|
804
|
-
let manager;
|
|
805
|
-
try {
|
|
806
|
-
manager = await import('@yixinkj/yixin-bridge-cli');
|
|
807
|
-
} catch (error) {
|
|
808
|
-
throw new Error(`缺少共享译心桥管理器 @yixinkj/yixin-bridge-cli:${error.message}`);
|
|
809
|
-
}
|
|
810
|
-
const result = await manager.ensureBridgeRuntime({ minVersion: registry.bridge.minVersion });
|
|
811
|
-
if (!result?.bridge_path || !fs.existsSync(result.bridge_path)) {
|
|
812
|
-
throw new Error('共享译心桥安装完成后未返回有效运行路径');
|
|
813
|
-
}
|
|
814
|
-
validateBridgeCapabilities(result, registry.bridge.requiredCapabilities || []);
|
|
815
|
-
return result;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* 校验当前共享桥是否具备本 Skill 的硬依赖能力。
|
|
820
|
-
* @param {object} result 共享桥管理器返回的运行时信息。
|
|
821
|
-
* @param {string[]} requiredCapabilities 本 Skill 声明的能力白名单。
|
|
822
|
-
* @returns {object} 校验通过后的原始运行时信息。
|
|
823
|
-
*/
|
|
824
|
-
function validateBridgeCapabilities(result, requiredCapabilities) {
|
|
825
|
-
const missing = requiredCapabilities.filter(
|
|
826
|
-
(capability) => !result.capabilities?.includes(capability)
|
|
827
|
-
);
|
|
828
|
-
if (missing.length > 0) {
|
|
829
|
-
// 具体 Skill 只校验自己声明的能力,避免无关运行时能力成为全局硬依赖。
|
|
830
|
-
throw new Error(`共享译心桥缺少必要能力:${missing.join(', ')}`);
|
|
831
|
-
}
|
|
832
|
-
return result;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* 把共享管理器的底层插件状态转换为用户可理解的有限提示。
|
|
837
|
-
* @param {object | null} sharedBridge 共享桥管理器返回的安装和插件状态。
|
|
838
|
-
* @returns {object | null} 需要展示的提示结构;无需提示时返回空值。
|
|
839
|
-
*/
|
|
840
|
-
function browserExtensionNotice(sharedBridge) {
|
|
841
|
-
if (!sharedBridge) return null;
|
|
842
|
-
const reloadStatus = sharedBridge.extension_reload?.status || 'not_requested';
|
|
843
|
-
const details = {
|
|
844
|
-
extension_version: sharedBridge.extension_version || null,
|
|
845
|
-
extension_path: sharedBridge.extension_path || null,
|
|
846
|
-
reload_status: reloadStatus
|
|
847
|
-
};
|
|
848
|
-
if (sharedBridge.extension_updated) {
|
|
849
|
-
return reloadStatus === 'reloaded'
|
|
850
|
-
? {
|
|
851
|
-
...details,
|
|
852
|
-
status: 'updated_reloaded',
|
|
853
|
-
message: `浏览器插件已更新至 ${details.extension_version || '最新版'},并已自动重新加载。`
|
|
854
|
-
}
|
|
855
|
-
: {
|
|
856
|
-
...details,
|
|
857
|
-
status: 'manual_reload_required',
|
|
858
|
-
message: `浏览器插件已更新至 ${details.extension_version || '最新版'},但未能自动加载。请先删除 Chrome 扩展管理页中的旧译心插件,再按下方“浏览器插件本地路径”导入新版并重新加载。`
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
if (reloadStatus === 'active_tasks_present') {
|
|
862
|
-
return {
|
|
863
|
-
...details,
|
|
864
|
-
status: 'update_deferred',
|
|
865
|
-
message: '检测到浏览器插件更新,但当前仍有采集任务,已暂缓更新。任务结束后请重新运行。'
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
if (reloadStatus === 'daemon_status_unavailable') {
|
|
869
|
-
return {
|
|
870
|
-
...details,
|
|
871
|
-
status: 'update_deferred',
|
|
872
|
-
message: '检测到浏览器插件更新,但暂时无法确认浏览器任务状态,未自动替换。请稍后重新运行。'
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
return null;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function emitBrowserExtensionNotice(sharedBridge) {
|
|
879
|
-
const notice = browserExtensionNotice(sharedBridge);
|
|
880
|
-
if (!notice) return;
|
|
881
|
-
// 原生 CLI 的 stdout 是机器解析的 JSON,因此提示只能写 stderr。
|
|
882
|
-
console.error(`[${SKILL_ID}] ${notice.message}`);
|
|
883
|
-
if (notice.extension_path) {
|
|
884
|
-
console.error(`[${SKILL_ID}] 浏览器插件本地路径:${notice.extension_path}`);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
338
|
async function main() {
|
|
889
339
|
const rawArgs = process.argv.slice(2);
|
|
890
340
|
if (isHelpRequest(rawArgs)) {
|
|
891
341
|
printHelp();
|
|
892
342
|
return;
|
|
893
343
|
}
|
|
894
|
-
const { skillPath, forwardedArgs } = extractSkillPath(rawArgs);
|
|
344
|
+
const { skillPath, forwardedArgs } = core.extractSkillPath(rawArgs);
|
|
895
345
|
if (forwardedArgs.length === 1 && ['--version', '-V', '-v'].includes(forwardedArgs[0])) {
|
|
896
346
|
console.log(`${SKILL_ID} ${SKILL_VERSION} (runtime ${RUNTIME_VERSION})`);
|
|
897
347
|
return;
|
|
@@ -971,25 +421,24 @@ async function main() {
|
|
|
971
421
|
process.exitCode = result.status ?? 1;
|
|
972
422
|
}
|
|
973
423
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
let current = fileURLToPath(import.meta.url);
|
|
978
|
-
try {
|
|
979
|
-
invoked = fs.realpathSync(invoked);
|
|
980
|
-
current = fs.realpathSync(current);
|
|
981
|
-
} catch {
|
|
982
|
-
// 某些文件系统无法解析符号链接时使用绝对路径比较。
|
|
983
|
-
}
|
|
984
|
-
return process.platform === 'win32'
|
|
985
|
-
? invoked.toLowerCase() === current.toLowerCase()
|
|
986
|
-
: invoked === current;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
if (isDirectExecution()) {
|
|
424
|
+
// import.meta.url 必须由这里传:内核若用自己的 import.meta.url 去比,结果永远是 false,
|
|
425
|
+
// 启动器会变成「导入即返回、什么都不做」,而且不报错,只会静默失效。
|
|
426
|
+
if (core.isDirectExecution(import.meta.url)) {
|
|
990
427
|
main().catch((error) => fail(error.message));
|
|
991
428
|
}
|
|
992
429
|
|
|
430
|
+
// 这几个与「我是谁」无关,内核版本逐字等价,直接转发即可,不必再包一层。
|
|
431
|
+
// browserExtensionNotice 的默认「未能自动加载」文案就是本仓这版(先删旧插件、再按路径导入),
|
|
432
|
+
// 内核把它当默认值,因此这里不需要传 manualReloadMessage。
|
|
433
|
+
export {
|
|
434
|
+
browserExtensionNotice,
|
|
435
|
+
commitRuntimeInstall,
|
|
436
|
+
compareVersions,
|
|
437
|
+
extractSkillPath,
|
|
438
|
+
parseSkillMetadata,
|
|
439
|
+
validateBridgeCapabilities
|
|
440
|
+
} from './launcher-core.js';
|
|
441
|
+
|
|
993
442
|
export {
|
|
994
443
|
BIN_DIR,
|
|
995
444
|
BUNDLED_SKILL_PATH,
|
|
@@ -998,15 +447,11 @@ export {
|
|
|
998
447
|
SKILL_ROOT,
|
|
999
448
|
SKILL_VERSION,
|
|
1000
449
|
VERSION_ROOT,
|
|
1001
|
-
browserExtensionNotice,
|
|
1002
|
-
compareVersions,
|
|
1003
450
|
detectTarget,
|
|
1004
451
|
discoverLegacySkillPaths,
|
|
1005
452
|
downloadVerifiedRelease,
|
|
1006
|
-
extractSkillPath,
|
|
1007
453
|
isHelpRequest,
|
|
1008
454
|
isInstalled,
|
|
1009
|
-
parseSkillMetadata,
|
|
1010
455
|
pruneOldRuntimeVersions,
|
|
1011
456
|
reportPrunedRuntimeVersions,
|
|
1012
457
|
releaseUrls,
|
|
@@ -1014,6 +459,5 @@ export {
|
|
|
1014
459
|
runtimeManifestUrl,
|
|
1015
460
|
runtimeManifestUrls,
|
|
1016
461
|
syncSkillInstallation,
|
|
1017
|
-
validateBridgeCapabilities,
|
|
1018
462
|
verifyReleaseArchive
|
|
1019
463
|
};
|