@yixinkj/priority-buyer-alert-cli 0.1.43 → 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 -808
- package/launcher-core.js +1457 -0
- package/package.json +3 -2
- package/skill/SKILL.md +31 -5
- 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
|
-
}
|
|
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
|
+
});
|
|
123
86
|
|
|
124
|
-
|
|
125
|
-
process.stdout.write(`${SKILL_DISPLAY_NAME} ${SKILL_VERSION}(Runtime ${RUNTIME_VERSION})
|
|
87
|
+
const releaseUrls = (target) => core.releaseUrls(releaseBaseUrls(), target);
|
|
126
88
|
|
|
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]
|
|
89
|
+
const runtimeManifestUrls = () => core.runtimeManifestUrls(releaseBaseUrls());
|
|
133
90
|
|
|
134
|
-
|
|
135
|
-
--skill-path <PATH> 本次实际读取的 SKILL.md 绝对路径
|
|
136
|
-
--version 显示版本
|
|
137
|
-
--help 显示本帮助
|
|
91
|
+
const runtimeManifestUrl = () => runtimeManifestUrls()[0];
|
|
138
92
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
--no-strict-bot 关闭机器人核验
|
|
147
|
-
--verify-all-replied 对全部已回复的重点买家做详情核验
|
|
148
|
-
--mark 推进提醒水位;只有心跳式调用才应使用
|
|
149
|
-
|
|
150
|
-
首响扫描选项(仅与 --first-response-scan 一起使用):
|
|
151
|
-
--since <HOURS> 只看最近 N 小时内有消息的询盘,默认 24
|
|
152
|
-
--limit <N> 本轮最多核验多少条会话详情,默认 100
|
|
153
|
-
|
|
154
|
-
环境变量:
|
|
155
|
-
PRIORITY_BUYER_ALERT_DETAIL_CONCURRENCY 详情核验并发度,默认 3,范围 1-8
|
|
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,542 +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
|
-
}
|
|
619
|
-
|
|
620
|
-
function runCommand(command, args) {
|
|
621
|
-
const result = spawnSync(command, args, {
|
|
622
|
-
encoding: 'utf8',
|
|
623
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
624
|
-
windowsHide: true
|
|
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
|
-
}
|
|
632
|
-
|
|
633
|
-
function extractArchive(archivePath, destination) {
|
|
634
|
-
fs.mkdirSync(destination, { recursive: true });
|
|
635
|
-
if (archivePath.endsWith('.tar.gz')) {
|
|
636
|
-
runCommand('tar', ['-xzf', archivePath, '-C', destination]);
|
|
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
|
-
}
|
|
655
|
-
|
|
656
|
-
function verifyExtracted(extractedDir, target) {
|
|
657
|
-
const manifest = JSON.parse(
|
|
658
|
-
fs.readFileSync(path.join(extractedDir, 'checksums.json'), 'utf8')
|
|
659
|
-
);
|
|
660
|
-
if (manifest.version !== RUNTIME_VERSION || manifest.platform !== target.id) {
|
|
661
|
-
throw new Error('下载包版本或平台与当前启动器不一致');
|
|
662
|
-
}
|
|
663
|
-
for (const name of target.binaries) {
|
|
664
|
-
const expected = manifest.files?.[name];
|
|
665
|
-
const filePath = path.join(extractedDir, name);
|
|
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
|
-
}
|
|
293
|
+
/** 帮助文案是本仓的命令面,不进共享内核。 */
|
|
294
|
+
function printHelp() {
|
|
295
|
+
process.stdout.write(`${SKILL_DISPLAY_NAME} ${SKILL_VERSION}(Runtime ${RUNTIME_VERSION})
|
|
677
296
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
}
|
|
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]
|
|
686
303
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
/// 不设成 1 是因为同一台机器会交替跑不同 dist-tag(例如 latest 与 beta),
|
|
692
|
-
/// 只留当前版会让每次切换都重新下载一遍。
|
|
693
|
-
const RUNTIME_VERSIONS_TO_KEEP = 2;
|
|
304
|
+
通用:
|
|
305
|
+
--skill-path <PATH> 本次实际读取的 SKILL.md 绝对路径
|
|
306
|
+
--version 显示版本
|
|
307
|
+
--help 显示本帮助
|
|
694
308
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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);
|
|
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 推进提醒水位;只有心跳式调用才应使用
|
|
735
319
|
|
|
736
|
-
|
|
737
|
-
|
|
320
|
+
首响扫描选项(仅与 --first-response-scan 一起使用):
|
|
321
|
+
--since <HOURS> 只看最近 N 小时内有消息的询盘,默认 24
|
|
322
|
+
--limit <N> 本轮最多核验多少条会话详情,默认 100
|
|
738
323
|
|
|
739
|
-
|
|
740
|
-
|
|
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;
|
|
750
|
-
}
|
|
324
|
+
环境变量:
|
|
325
|
+
PRIORITY_BUYER_ALERT_DETAIL_CONCURRENCY 详情核验并发度,默认 3,范围 1-8
|
|
751
326
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
*
|
|
755
|
-
* 放在「已安装」的快路径上也要跑:只在新装时清理的话,一台停止升级的机器会把
|
|
756
|
-
* 历史版本永远留着。
|
|
757
|
-
*/
|
|
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;
|
|
327
|
+
本 Skill 只读:只提醒,不发送消息、不修改任何页面状态。
|
|
328
|
+
`);
|
|
764
329
|
}
|
|
765
330
|
|
|
766
331
|
/**
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* 同一 Skill 的两个实例可能同时首次安装同一版本,各自下载到自己的 staging。
|
|
770
|
-
* 版本目录按版本号钉死,内容又逐个核对过大小与 sha256,谁装出来的字节都一样,
|
|
771
|
-
* 因此没有必要抢:对方已经装好就直接用他那份。
|
|
772
|
-
*
|
|
773
|
-
* 抢的代价是真的。原来的写法是无条件 `rmSync` 再 `renameSync`,会删掉对方刚
|
|
774
|
-
* 装好、正准备执行的目录:POSIX 上对方若已 spawn 还能活下来,但两步之间那一瞬
|
|
775
|
-
* 版本目录是不存在的;Windows 上更直接——删正在运行的 exe 会抛错,把这次启动
|
|
776
|
-
* 整个带崩。
|
|
332
|
+
* 启动流程。
|
|
777
333
|
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
* @returns {'installed' | 'kept_existing'} 就位的是本进程这份还是别人那份。
|
|
782
|
-
*/
|
|
783
|
-
function commitRuntimeInstall(stagingRoot, versionRoot, isVersionReady) {
|
|
784
|
-
if (isVersionReady()) return 'kept_existing';
|
|
785
|
-
fs.rmSync(versionRoot, { recursive: true, force: true });
|
|
786
|
-
try {
|
|
787
|
-
fs.renameSync(stagingRoot, versionRoot);
|
|
788
|
-
} catch (error) {
|
|
789
|
-
// 对方恰好在这两步之间把同版本装好了。抢输不该让本次启动失败。
|
|
790
|
-
if (!isVersionReady()) throw error;
|
|
791
|
-
return 'kept_existing';
|
|
792
|
-
}
|
|
793
|
-
return 'installed';
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
async function ensureInstalled(target) {
|
|
797
|
-
if (isInstalled(target)) {
|
|
798
|
-
prepareMacBinaries(target);
|
|
799
|
-
reportPrunedRuntimeVersions();
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `${SKILL_ID}-install-`));
|
|
803
|
-
const archivePath = path.join(tempRoot, target.archive);
|
|
804
|
-
const releaseManifestPath = path.join(tempRoot, 'runtime-manifest.json');
|
|
805
|
-
const extractedDir = path.join(tempRoot, 'extracted');
|
|
806
|
-
const stagingRoot = `${VERSION_ROOT}.staging-${process.pid}`;
|
|
807
|
-
try {
|
|
808
|
-
await downloadVerifiedRelease(target, releaseManifestPath, archivePath);
|
|
809
|
-
extractArchive(archivePath, extractedDir);
|
|
810
|
-
const files = verifyExtracted(extractedDir, target);
|
|
811
|
-
|
|
812
|
-
fs.mkdirSync(path.dirname(VERSION_ROOT), { recursive: true });
|
|
813
|
-
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
814
|
-
fs.mkdirSync(path.join(stagingRoot, 'bin'), { recursive: true });
|
|
815
|
-
for (const name of target.binaries) {
|
|
816
|
-
fs.copyFileSync(path.join(extractedDir, name), path.join(stagingRoot, 'bin', name));
|
|
817
|
-
}
|
|
818
|
-
fs.writeFileSync(
|
|
819
|
-
path.join(stagingRoot, 'installed.json'),
|
|
820
|
-
`${JSON.stringify({ version: RUNTIME_VERSION, platform: target.id, files }, null, 2)}\n`
|
|
821
|
-
);
|
|
822
|
-
|
|
823
|
-
commitRuntimeInstall(stagingRoot, VERSION_ROOT, () => isInstalled(target));
|
|
824
|
-
prepareMacBinaries(target);
|
|
825
|
-
reportPrunedRuntimeVersions();
|
|
826
|
-
} finally {
|
|
827
|
-
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
828
|
-
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
async function ensureSharedBridge() {
|
|
833
|
-
let manager;
|
|
834
|
-
try {
|
|
835
|
-
manager = await import('@yixinkj/yixin-bridge-cli');
|
|
836
|
-
} catch (error) {
|
|
837
|
-
throw new Error(`缺少共享译心桥管理器 @yixinkj/yixin-bridge-cli:${error.message}`);
|
|
838
|
-
}
|
|
839
|
-
const result = await manager.ensureBridgeRuntime({ minVersion: registry.bridge.minVersion });
|
|
840
|
-
if (!result?.bridge_path || !fs.existsSync(result.bridge_path)) {
|
|
841
|
-
throw new Error('共享译心桥安装完成后未返回有效运行路径');
|
|
842
|
-
}
|
|
843
|
-
validateBridgeCapabilities(result, registry.bridge.requiredCapabilities || []);
|
|
844
|
-
return result;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* 校验当前共享桥是否具备本 Skill 的硬依赖能力。
|
|
849
|
-
* @param {object} result 共享桥管理器返回的运行时信息。
|
|
850
|
-
* @param {string[]} requiredCapabilities 本 Skill 声明的能力白名单。
|
|
851
|
-
* @returns {object} 校验通过后的原始运行时信息。
|
|
334
|
+
* 这段也没有进共享内核:三个仓的退出协议不同(本仓在 skill_path_required 时置
|
|
335
|
+
* exitCode = 1,另外两仓直接 return),文案里的动作名也不同。把它模板化会让「改一句文案」
|
|
336
|
+
* 变成「改共享库」,更糟的是会悄悄统一退出码——退出码是调用方(智能体)的协议。
|
|
852
337
|
*/
|
|
853
|
-
function validateBridgeCapabilities(result, requiredCapabilities) {
|
|
854
|
-
const missing = requiredCapabilities.filter(
|
|
855
|
-
(capability) => !result.capabilities?.includes(capability)
|
|
856
|
-
);
|
|
857
|
-
if (missing.length > 0) {
|
|
858
|
-
// 具体 Skill 只校验自己声明的能力,避免无关运行时能力成为全局硬依赖。
|
|
859
|
-
throw new Error(`共享译心桥缺少必要能力:${missing.join(', ')}`);
|
|
860
|
-
}
|
|
861
|
-
return result;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* 把共享管理器的底层插件状态转换为用户可理解的有限提示。
|
|
866
|
-
* @param {object | null} sharedBridge 共享桥管理器返回的安装和插件状态。
|
|
867
|
-
* @returns {object | null} 需要展示的提示结构;无需提示时返回空值。
|
|
868
|
-
*/
|
|
869
|
-
function browserExtensionNotice(sharedBridge) {
|
|
870
|
-
if (!sharedBridge) return null;
|
|
871
|
-
const reloadStatus = sharedBridge.extension_reload?.status || 'not_requested';
|
|
872
|
-
const details = {
|
|
873
|
-
extension_version: sharedBridge.extension_version || null,
|
|
874
|
-
extension_path: sharedBridge.extension_path || null,
|
|
875
|
-
reload_status: reloadStatus
|
|
876
|
-
};
|
|
877
|
-
if (sharedBridge.extension_updated) {
|
|
878
|
-
return reloadStatus === 'reloaded'
|
|
879
|
-
? {
|
|
880
|
-
...details,
|
|
881
|
-
status: 'updated_reloaded',
|
|
882
|
-
message: `浏览器插件已更新至 ${details.extension_version || '最新版'},并已自动重新加载。`
|
|
883
|
-
}
|
|
884
|
-
: {
|
|
885
|
-
...details,
|
|
886
|
-
status: 'manual_reload_required',
|
|
887
|
-
message: `浏览器插件已更新至 ${details.extension_version || '最新版'},但未能自动加载。请先删除 Chrome 扩展管理页中的旧译心插件,再按下方“浏览器插件本地路径”导入新版并重新加载。`
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
if (reloadStatus === 'active_tasks_present') {
|
|
891
|
-
return {
|
|
892
|
-
...details,
|
|
893
|
-
status: 'update_deferred',
|
|
894
|
-
message: '检测到浏览器插件更新,但当前仍有采集任务,已暂缓更新。任务结束后请重新运行。'
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
if (reloadStatus === 'daemon_status_unavailable') {
|
|
898
|
-
return {
|
|
899
|
-
...details,
|
|
900
|
-
status: 'update_deferred',
|
|
901
|
-
message: '检测到浏览器插件更新,但暂时无法确认浏览器任务状态,未自动替换。请稍后重新运行。'
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
return null;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function emitBrowserExtensionNotice(sharedBridge) {
|
|
908
|
-
const notice = browserExtensionNotice(sharedBridge);
|
|
909
|
-
if (!notice) return;
|
|
910
|
-
// 原生 CLI 的 stdout 是机器解析的 JSON,因此提示只能写 stderr。
|
|
911
|
-
console.error(`[${SKILL_ID}] ${notice.message}`);
|
|
912
|
-
if (notice.extension_path) {
|
|
913
|
-
console.error(`[${SKILL_ID}] 浏览器插件本地路径:${notice.extension_path}`);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
338
|
async function main() {
|
|
918
339
|
const rawArgs = process.argv.slice(2);
|
|
919
340
|
if (isHelpRequest(rawArgs)) {
|
|
920
341
|
printHelp();
|
|
921
342
|
return;
|
|
922
343
|
}
|
|
923
|
-
const { skillPath, forwardedArgs } = extractSkillPath(rawArgs);
|
|
344
|
+
const { skillPath, forwardedArgs } = core.extractSkillPath(rawArgs);
|
|
924
345
|
if (forwardedArgs.length === 1 && ['--version', '-V', '-v'].includes(forwardedArgs[0])) {
|
|
925
346
|
console.log(`${SKILL_ID} ${SKILL_VERSION} (runtime ${RUNTIME_VERSION})`);
|
|
926
347
|
return;
|
|
@@ -1000,25 +421,24 @@ async function main() {
|
|
|
1000
421
|
process.exitCode = result.status ?? 1;
|
|
1001
422
|
}
|
|
1002
423
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
let current = fileURLToPath(import.meta.url);
|
|
1007
|
-
try {
|
|
1008
|
-
invoked = fs.realpathSync(invoked);
|
|
1009
|
-
current = fs.realpathSync(current);
|
|
1010
|
-
} catch {
|
|
1011
|
-
// 某些文件系统无法解析符号链接时使用绝对路径比较。
|
|
1012
|
-
}
|
|
1013
|
-
return process.platform === 'win32'
|
|
1014
|
-
? invoked.toLowerCase() === current.toLowerCase()
|
|
1015
|
-
: invoked === current;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
if (isDirectExecution()) {
|
|
424
|
+
// import.meta.url 必须由这里传:内核若用自己的 import.meta.url 去比,结果永远是 false,
|
|
425
|
+
// 启动器会变成「导入即返回、什么都不做」,而且不报错,只会静默失效。
|
|
426
|
+
if (core.isDirectExecution(import.meta.url)) {
|
|
1019
427
|
main().catch((error) => fail(error.message));
|
|
1020
428
|
}
|
|
1021
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
|
+
|
|
1022
442
|
export {
|
|
1023
443
|
BIN_DIR,
|
|
1024
444
|
BUNDLED_SKILL_PATH,
|
|
@@ -1027,16 +447,11 @@ export {
|
|
|
1027
447
|
SKILL_ROOT,
|
|
1028
448
|
SKILL_VERSION,
|
|
1029
449
|
VERSION_ROOT,
|
|
1030
|
-
browserExtensionNotice,
|
|
1031
|
-
commitRuntimeInstall,
|
|
1032
|
-
compareVersions,
|
|
1033
450
|
detectTarget,
|
|
1034
451
|
discoverLegacySkillPaths,
|
|
1035
452
|
downloadVerifiedRelease,
|
|
1036
|
-
extractSkillPath,
|
|
1037
453
|
isHelpRequest,
|
|
1038
454
|
isInstalled,
|
|
1039
|
-
parseSkillMetadata,
|
|
1040
455
|
pruneOldRuntimeVersions,
|
|
1041
456
|
reportPrunedRuntimeVersions,
|
|
1042
457
|
releaseUrls,
|
|
@@ -1044,6 +459,5 @@ export {
|
|
|
1044
459
|
runtimeManifestUrl,
|
|
1045
460
|
runtimeManifestUrls,
|
|
1046
461
|
syncSkillInstallation,
|
|
1047
|
-
validateBridgeCapabilities,
|
|
1048
462
|
verifyReleaseArchive
|
|
1049
463
|
};
|