@yixinkj/inquiry-opening-coach-cli 0.3.1
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 +528 -0
- package/launcher-core.js +1457 -0
- package/package.json +30 -0
- package/skill/SKILL.md +550 -0
- package/skill/agents/openai.yaml +6 -0
- package/skill/references/country-styles.md +16 -0
- package/skill/references/opening-rules.md +38 -0
- package/skills.json +30 -0
package/index.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
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
|
+
// 那条日志路径只在「确实删掉了旧版本」时才走到,联调机上是空目录,一路混过了测试。
|
|
15
|
+
//
|
|
16
|
+
// 本文件里的 launcher-core.js 是副本,唯一源在 priority-buyer-alert。
|
|
17
|
+
// **禁止在本仓就地修改它**:launcher-core-integrity.test.js 用 sha256 守着,
|
|
18
|
+
// prepack 也会中止发布。要改就回源仓改,让内核的 58 条测试先过一遍。
|
|
19
|
+
//
|
|
20
|
+
// 它必须出现在 package.json 的 files 里,否则发布出去的包在 npx 第一行就
|
|
21
|
+
// ERR_MODULE_NOT_FOUND。package-contents.test.js 用真实的 npm pack 清单守着这一条。
|
|
22
|
+
import * as core from './launcher-core.js';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const packageJson = require('./package.json');
|
|
26
|
+
const registry = require('./skills.json');
|
|
27
|
+
|
|
28
|
+
const VERSION = packageJson.version;
|
|
29
|
+
const SKILL_VERSION = registry.skill.version || VERSION;
|
|
30
|
+
const SKILL_ID = registry.skill.id;
|
|
31
|
+
// 本 Skill 的 frontmatter name 就是目录名 create-inquiry-openers,与内部 ID 不同。
|
|
32
|
+
// 本仓没有改过展示名,因此不需要 priority-buyer-alert 那张历史展示名白名单——
|
|
33
|
+
// 那张表属于那个 Skill 自己的改名历史,照抄过来只会凭空放宽身份校验。
|
|
34
|
+
const SKILL_DISPLAY_NAME = registry.skill.name;
|
|
35
|
+
const RUNTIME_VERSION = registry.runtimeVersion || VERSION;
|
|
36
|
+
const OSS_RELEASE_BASE_URL =
|
|
37
|
+
`https://broswer-plugs1.oss-cn-beijing.aliyuncs.com/releases/${SKILL_ID}/v${RUNTIME_VERSION}`;
|
|
38
|
+
const R2_RELEASE_BASE_URL =
|
|
39
|
+
`https://pub-60851fbb424a46c9a15b0ffce2ffea86.r2.dev/releases/${SKILL_ID}/v${RUNTIME_VERSION}`;
|
|
40
|
+
// 与 tools/release/write_release_manifest.mjs 写进清单的 schema 逐字一致。
|
|
41
|
+
// 它是本 Skill 自己的字符串:多个 Skill 的产物放在同一个桶里,只比版本号会让
|
|
42
|
+
// 「下错 Skill 的包」一路通过校验。
|
|
43
|
+
const RUNTIME_MANIFEST_SCHEMA = 'inquiry_opening_coach_runtime_manifest_v1';
|
|
44
|
+
const PACKAGE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
// 已发布的包里 `skill/` 由 prepack 的 sync-skill-assets.mjs 整份生成;直接在源码仓库里
|
|
46
|
+
// 跑 index.js 时它不存在,此时回退到仓库内的 Skill 源目录,避免联调必须先手工同步一次。
|
|
47
|
+
// 发布产物中不存在仓库路径,因此这个回退不会让线上包读到仓库副本。
|
|
48
|
+
const PACKED_SKILL_PATH = path.join(PACKAGE_ROOT, 'skill', 'SKILL.md');
|
|
49
|
+
const REPOSITORY_SKILL_PATH = path.join(
|
|
50
|
+
PACKAGE_ROOT,
|
|
51
|
+
'..',
|
|
52
|
+
'..',
|
|
53
|
+
'skills',
|
|
54
|
+
'create-inquiry-openers',
|
|
55
|
+
'SKILL.md'
|
|
56
|
+
);
|
|
57
|
+
const BUNDLED_SKILL_PATH = fs.existsSync(PACKED_SKILL_PATH)
|
|
58
|
+
? PACKED_SKILL_PATH
|
|
59
|
+
: REPOSITORY_SKILL_PATH;
|
|
60
|
+
const SKILL_VERSION_MARKER = `.${SKILL_ID}-version.json`;
|
|
61
|
+
// 注意:SKILL_ROOT 下面除了 runtime/ 还住着用户的记忆库(banks/、config.json)。
|
|
62
|
+
// 那是用户从 0.2 起累积的资料,任何发布都不许动它——本文件只写 runtime/ 这一层。
|
|
63
|
+
const SKILL_ROOT = path.join(os.homedir(), '.yixin', SKILL_ID);
|
|
64
|
+
const RUNTIME_ROOT = path.join(SKILL_ROOT, 'runtime');
|
|
65
|
+
const VERSION_ROOT = path.join(RUNTIME_ROOT, 'versions', `v${RUNTIME_VERSION}`);
|
|
66
|
+
const BIN_DIR = path.join(VERSION_ROOT, 'bin');
|
|
67
|
+
const INSTALL_MARKER = path.join(VERSION_ROOT, 'installed.json');
|
|
68
|
+
|
|
69
|
+
/* ------------------------------------------------------------------ *
|
|
70
|
+
* 绑定层
|
|
71
|
+
*
|
|
72
|
+
* 这一层只做一件事:把上面那批「我是谁」的常量补给共享内核。里面不允许有判断逻辑,
|
|
73
|
+
* 有判断的部分(身份校验、退出协议、帮助文案)一律留在下面的本仓代码里。
|
|
74
|
+
* ------------------------------------------------------------------ */
|
|
75
|
+
|
|
76
|
+
const fail = core.createFailHandler({ logPrefix: SKILL_ID });
|
|
77
|
+
|
|
78
|
+
const emitStatus = core.createStatusEmitter({ skillVersion: SKILL_VERSION });
|
|
79
|
+
|
|
80
|
+
// 本仓把「不带任何参数」当作帮助请求。理由:这个 CLI 是子命令式的(retain / recall /
|
|
81
|
+
// fetch / validate …),没有任何可用的默认动作,裸调一次的唯一合理产物就是命令面。
|
|
82
|
+
// 而帮助必须在 Skill OTA 和运行时下载之前返回——若判成「不是帮助」,裸调会先跑一遍
|
|
83
|
+
// Skill 发现、再下载几十兆运行时,最后只为让 clap 打印一句 usage 错误。
|
|
84
|
+
// 这个值必须显式写出来:public-customer-reactivation 的答案是相反的,内核因此不给默认值。
|
|
85
|
+
const isHelpRequest = (args) => core.isHelpRequest(args, { emptyArgsMeansHelp: true });
|
|
86
|
+
|
|
87
|
+
// 环境变量的**名字**属于本仓身份,留在这里;「显式覆盖主源就不追加默认备用源」那条规则在内核里。
|
|
88
|
+
// 前缀 INQUIRY_OPENING_COACH_ 与 Rust 侧一致(src/store/storage.rs 的
|
|
89
|
+
// INQUIRY_OPENING_COACH_DATA_DIR)。必须在函数内部读 process.env:模块加载时读会让
|
|
90
|
+
// 测试和联调无法在运行中改源。
|
|
91
|
+
const releaseBaseUrls = () =>
|
|
92
|
+
core.resolveReleaseBaseUrls({
|
|
93
|
+
defaultPrimary: OSS_RELEASE_BASE_URL,
|
|
94
|
+
defaultFallbacks: [R2_RELEASE_BASE_URL],
|
|
95
|
+
overridePrimary: process.env.INQUIRY_OPENING_COACH_RELEASE_BASE_URL,
|
|
96
|
+
overrideFallbacks: process.env.INQUIRY_OPENING_COACH_FALLBACK_BASE_URLS
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const releaseUrls = (target) => core.releaseUrls(releaseBaseUrls(), target);
|
|
100
|
+
|
|
101
|
+
const runtimeManifestUrls = () => core.runtimeManifestUrls(releaseBaseUrls());
|
|
102
|
+
|
|
103
|
+
const runtimeManifestUrl = () => runtimeManifestUrls()[0];
|
|
104
|
+
|
|
105
|
+
const verifyReleaseArchive = (manifest, target, archivePath) =>
|
|
106
|
+
core.verifyReleaseArchive(manifest, target, archivePath, {
|
|
107
|
+
manifestSchema: RUNTIME_MANIFEST_SCHEMA,
|
|
108
|
+
runtimeVersion: RUNTIME_VERSION
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// options 放在最后展开:测试会注入 baseUrls / downloadFile,注入值必须能盖掉默认值。
|
|
112
|
+
const downloadVerifiedRelease = (target, releaseManifestPath, archivePath, options = {}) =>
|
|
113
|
+
core.downloadVerifiedRelease(target, releaseManifestPath, archivePath, {
|
|
114
|
+
baseUrls: releaseBaseUrls(),
|
|
115
|
+
downloadFile: (url, outputPath) =>
|
|
116
|
+
core.download(url, outputPath, { userAgent: `@yixinkj/${SKILL_ID}-cli` }),
|
|
117
|
+
verifyArchive: verifyReleaseArchive,
|
|
118
|
+
...options
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// registry.skill.bin 是 inquiry-opening-coach-cli(Cargo 的 [[bin]] name),
|
|
122
|
+
// 与 SKILL_ID 差一个 -cli 后缀,不能互相顶替:内核会拼成
|
|
123
|
+
// `${bin}-${平台id}${exe}`,正是 build_local.sh 放进压缩包的那个文件名。
|
|
124
|
+
const detectTarget = () =>
|
|
125
|
+
core.detectTarget({ platforms: registry.platforms, binName: registry.skill.bin });
|
|
126
|
+
|
|
127
|
+
const isInstalled = (target) =>
|
|
128
|
+
core.isRuntimeInstalled({
|
|
129
|
+
target,
|
|
130
|
+
binDir: BIN_DIR,
|
|
131
|
+
installMarkerPath: INSTALL_MARKER,
|
|
132
|
+
runtimeVersion: RUNTIME_VERSION
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 位置参数原样保留,index.test.js 里 `pruneOldRuntimeVersions(root, 'v0.0.3', 2)` 直接可用。
|
|
136
|
+
const pruneOldRuntimeVersions = (
|
|
137
|
+
versionsRoot = path.dirname(VERSION_ROOT),
|
|
138
|
+
active = path.basename(VERSION_ROOT),
|
|
139
|
+
keepCount = core.RUNTIME_VERSIONS_TO_KEEP
|
|
140
|
+
) => core.pruneOldRuntimeVersions({ versionsRoot, active, keepCount });
|
|
141
|
+
|
|
142
|
+
// logPrefix 必填就是那次事故的正解:漏传时在入口报「缺少 logPrefix」,
|
|
143
|
+
// 而不是等到真的删掉了旧版本、走到打印那一行才崩掉整个启动。
|
|
144
|
+
// 这里刻意不传 log:内核的默认值是在每次调用时取 console.error,
|
|
145
|
+
// 测试替换 console.error 才能截到这行日志。
|
|
146
|
+
const reportPrunedRuntimeVersions = (
|
|
147
|
+
versionsRoot = path.dirname(VERSION_ROOT),
|
|
148
|
+
active = path.basename(VERSION_ROOT),
|
|
149
|
+
keepCount = core.RUNTIME_VERSIONS_TO_KEEP
|
|
150
|
+
) => core.reportPrunedRuntimeVersions({ versionsRoot, active, keepCount, logPrefix: SKILL_ID });
|
|
151
|
+
|
|
152
|
+
const ensureInstalled = (target) =>
|
|
153
|
+
core.ensureRuntimeInstalled({
|
|
154
|
+
target,
|
|
155
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
156
|
+
versionRoot: VERSION_ROOT,
|
|
157
|
+
binDir: BIN_DIR,
|
|
158
|
+
tempDirPrefix: `${SKILL_ID}-install-`,
|
|
159
|
+
logPrefix: SKILL_ID,
|
|
160
|
+
downloadRelease: downloadVerifiedRelease
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 桥的 import 由这里注入,内核因此不静态依赖任何第三方包。
|
|
164
|
+
//
|
|
165
|
+
// requiredCapabilities 取本仓 skills.json 的声明,当前是空的,且**必须**是空的:
|
|
166
|
+
// runtime-dependencies.json 里写清楚了理由——本 Skill 只在询盘详情页读 DOM 并在页面
|
|
167
|
+
// 上下文内发一次同源 POST,不做 frame 定位、不截图、不复用调试会话。从别的仓照抄一份
|
|
168
|
+
// 非空清单(例如 frame_url_v1)会在插件升级改名或合并能力时变成假硬依赖,
|
|
169
|
+
// 把本来能跑的 Skill 直接挡在门外。任务协议底线由 yixin-browser 的 ensure_ready 自己强制。
|
|
170
|
+
const ensureSharedBridge = () =>
|
|
171
|
+
core.ensureSharedBridge({
|
|
172
|
+
importBridge: () => import('@yixinkj/yixin-bridge-cli'),
|
|
173
|
+
minVersion: registry.bridge.minVersion,
|
|
174
|
+
requiredCapabilities: registry.bridge.requiredCapabilities || []
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const emitBrowserExtensionNotice = (sharedBridge) =>
|
|
178
|
+
core.emitBrowserExtensionNotice(sharedBridge, { logPrefix: SKILL_ID });
|
|
179
|
+
|
|
180
|
+
// 识别特征属于本仓:npm 包名出现在正文里(SKILL.md 的运行入口就是 npx 那一行),
|
|
181
|
+
// 或 frontmatter 的 name 正是本 Skill 的目录名。
|
|
182
|
+
const discoverLegacySkillPaths = (homeDir = os.homedir()) =>
|
|
183
|
+
core.discoverLegacySkillPaths({
|
|
184
|
+
homeDir,
|
|
185
|
+
matches: (content) =>
|
|
186
|
+
content.includes(`@yixinkj/${SKILL_ID}-cli`) ||
|
|
187
|
+
new RegExp(`^name:\\s*${SKILL_DISPLAY_NAME}\\s*$`, 'm').test(content)
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// 'require_argument':没传 --skill-path 就永远不放行,并在结论里点名参数和具体路径。
|
|
191
|
+
//
|
|
192
|
+
// 本 Skill 是第四个接线的仓,此前从没有过 npm 启动器(旧形态是 Codex 插件直接调
|
|
193
|
+
// bin/ 下的二进制),**因此不存在需要保持兼容的历史退出协议**——另外两仓保留
|
|
194
|
+
// 'restart_required' 只是为了不在迁移里顺手改掉既有行为,本仓没有这个包袱,
|
|
195
|
+
// 直接取正确的那个语义。
|
|
196
|
+
//
|
|
197
|
+
// 而且本 Skill 一轮对话里要调十几次 CLI(fetch → recall → validate → retain …)。
|
|
198
|
+
// 'restart_required' 给出的是含糊的「已更新,请重新试一次」,可缺的明明是参数不是同步,
|
|
199
|
+
// 正好会诱导调用方原样重跑——重试一万次也不会成功,而且是每条命令都重试一遍。
|
|
200
|
+
const resolveSkillGate = (skillPath, { homeDir = os.homedir(), syncOptions = {} } = {}) =>
|
|
201
|
+
core.resolveSkillGate(skillPath, {
|
|
202
|
+
syncSkill: (found) => syncSkillInstallation(found, syncOptions),
|
|
203
|
+
discoverSkillPaths: () => discoverLegacySkillPaths(homeDir),
|
|
204
|
+
discoveredPathPolicy: 'require_argument'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/* ------------------------------------------------------------------ *
|
|
208
|
+
* 本仓自己的逻辑
|
|
209
|
+
* ------------------------------------------------------------------ */
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 列出内置 Skill 目录里除 SKILL.md 之外要整棵同步的资源目录。
|
|
213
|
+
*
|
|
214
|
+
* 按目录扫描而不是按文件名列举:本 Skill 的 `references/country-styles.md` 和
|
|
215
|
+
* `references/opening-rules.md` 是开场白的核心依据,`agents/openai.yaml` 是宿主适配。
|
|
216
|
+
* 写死清单的话,以后新增一份参考文件不会有任何报错——SKILL.md 照样能跑,只是文案
|
|
217
|
+
* 没了依据,输出慢慢变差,这种失效最难被发现。scripts/sync-skill-assets.mjs 出于同样
|
|
218
|
+
* 的理由复制整棵目录,两边保持一致。
|
|
219
|
+
* @param {string} bundledSkillRoot 内置 SKILL.md 所在目录。
|
|
220
|
+
* @returns {string[]} 排序后的一级子目录名。
|
|
221
|
+
*/
|
|
222
|
+
function bundledAssetDirNames(bundledSkillRoot) {
|
|
223
|
+
return fs
|
|
224
|
+
.readdirSync(bundledSkillRoot, { withFileTypes: true })
|
|
225
|
+
.filter((entry) => entry.isDirectory())
|
|
226
|
+
.map((entry) => entry.name)
|
|
227
|
+
.sort();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 把 npm 包内置的 Skill 同步到当前智能体实际使用的安装位置。
|
|
232
|
+
* 本地版本更高时拒绝降级;任一校验失败时整体回滚,不留下半个新版 Skill。
|
|
233
|
+
*
|
|
234
|
+
* 这个函数没有被提取进共享内核:四个仓真正不同的地方就在这里——同步的产物不同、
|
|
235
|
+
* 身份判定不同、版本来源不同。做成一个带三套插件点的通用函数只会变成隐式行为的温床。
|
|
236
|
+
* 内部真正共用且最危险的四块(路径校验、内置包校验、registry 预演/回读、可回滚事务)走内核。
|
|
237
|
+
*
|
|
238
|
+
* 本仓与 priority-buyer-alert 的实质差别:**除 SKILL.md 外还要整棵同步 references/ 和
|
|
239
|
+
* agents/**。只换 SKILL.md 会得到「新版说明 + 旧版参考文件」的组合,既不报错也不对。
|
|
240
|
+
* 因此这里用内核的 trees 参数做整棵回滚——只还原被改动的文件会把新版新增的文件留在原地。
|
|
241
|
+
* @param {string} skillPath 本次实际读取的 SKILL.md 绝对路径。
|
|
242
|
+
* @param {object} [options] 测试注入的内置文件路径。
|
|
243
|
+
* @returns {object} 同步结论与版本信息。
|
|
244
|
+
*/
|
|
245
|
+
function syncSkillInstallation(skillPath, { bundledSkillPath = BUNDLED_SKILL_PATH } = {}) {
|
|
246
|
+
const resolvedSkillPath = core.resolveSkillMdPath(skillPath);
|
|
247
|
+
const { content: bundledSkill, metadata: bundledMetadata } =
|
|
248
|
+
core.loadBundledSkill(bundledSkillPath);
|
|
249
|
+
// 内核只检查包自身完整性(文件在不在、name/description 有没有),身份判定留在本仓。
|
|
250
|
+
if (bundledMetadata.name !== SKILL_DISPLAY_NAME) {
|
|
251
|
+
throw new Error('内置 SKILL.md 名称不匹配');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const currentSkill = fs.readFileSync(resolvedSkillPath, 'utf8');
|
|
255
|
+
const currentSkillName = core.parseSkillMetadata(currentSkill).name;
|
|
256
|
+
// 也认内部 ID:早期安装副本的 frontmatter 可能写的是 inquiry-opening-coach,
|
|
257
|
+
// 允许它被目录名平滑升级,避免 OTA 被自身名称校验永久挡住。
|
|
258
|
+
if (currentSkillName !== SKILL_ID && currentSkillName !== SKILL_DISPLAY_NAME) {
|
|
259
|
+
throw new Error(`--skill-path 指向的不是${SKILL_DISPLAY_NAME} Skill,已拒绝覆盖`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const skillDir = path.dirname(resolvedSkillPath);
|
|
263
|
+
const bundledSkillRoot = path.dirname(path.resolve(bundledSkillPath));
|
|
264
|
+
const registryPreview = core.registryUpdatePreview(resolvedSkillPath, bundledMetadata, {
|
|
265
|
+
targetVersion: SKILL_VERSION
|
|
266
|
+
});
|
|
267
|
+
const currentVersion = core.readInstalledSkillVersion(skillDir, {
|
|
268
|
+
markerFileName: SKILL_VERSION_MARKER,
|
|
269
|
+
registryVersion: registryPreview.currentVersion
|
|
270
|
+
});
|
|
271
|
+
if (core.compareVersions(currentVersion, SKILL_VERSION) > 0) {
|
|
272
|
+
return {
|
|
273
|
+
action: 'local_newer',
|
|
274
|
+
skillPath: resolvedSkillPath,
|
|
275
|
+
currentVersion,
|
|
276
|
+
targetVersion: SKILL_VERSION
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const markerPath = path.join(skillDir, SKILL_VERSION_MARKER);
|
|
281
|
+
// 写入内容和比对内容必须来自同一个函数,否则「写时带换行、比时不带」会让每次启动都判定有变化。
|
|
282
|
+
const markerContent = core.versionMarkerContent(SKILL_VERSION);
|
|
283
|
+
const skillChanged = Buffer.from(currentSkill).compare(Buffer.from(bundledSkill)) !== 0;
|
|
284
|
+
const markerChanged =
|
|
285
|
+
!fs.existsSync(markerPath) || fs.readFileSync(markerPath, 'utf8') !== markerContent;
|
|
286
|
+
// 摘要相同就不动那棵树:每次启动都重写一遍资源目录会把 mtime 刷成现在,
|
|
287
|
+
// 也会让「这次到底改了什么」无从判断。
|
|
288
|
+
const changedAssets = bundledAssetDirNames(bundledSkillRoot)
|
|
289
|
+
.map((name) => ({
|
|
290
|
+
name,
|
|
291
|
+
source: path.join(bundledSkillRoot, name),
|
|
292
|
+
target: path.join(skillDir, name)
|
|
293
|
+
}))
|
|
294
|
+
.filter((asset) => core.treeDigest(asset.target) !== core.treeDigest(asset.source));
|
|
295
|
+
|
|
296
|
+
if (!skillChanged && !markerChanged && !registryPreview.changed && changedAssets.length === 0) {
|
|
297
|
+
return {
|
|
298
|
+
action: 'current',
|
|
299
|
+
skillPath: resolvedSkillPath,
|
|
300
|
+
currentVersion,
|
|
301
|
+
targetVersion: SKILL_VERSION,
|
|
302
|
+
registryPath: registryPreview.registryPath
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
core.runWithRollback({
|
|
307
|
+
files: [
|
|
308
|
+
resolvedSkillPath,
|
|
309
|
+
markerPath,
|
|
310
|
+
...(registryPreview.registryPath ? [registryPreview.registryPath] : [])
|
|
311
|
+
],
|
|
312
|
+
trees: changedAssets.map((asset) => asset.target),
|
|
313
|
+
apply: () => {
|
|
314
|
+
if (skillChanged) fs.writeFileSync(resolvedSkillPath, bundledSkill);
|
|
315
|
+
if (markerChanged) fs.writeFileSync(markerPath, markerContent);
|
|
316
|
+
for (const asset of changedAssets) {
|
|
317
|
+
// restoreTree 先整棵删再按快照写回,正好等于「用内置那棵覆盖安装位置那棵」。
|
|
318
|
+
core.restoreTree(asset.target, core.snapshotTree(asset.source));
|
|
319
|
+
}
|
|
320
|
+
if (registryPreview.changed) {
|
|
321
|
+
fs.writeFileSync(registryPreview.registryPath, registryPreview.content);
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
// 自检必须在事务内:校验失败同样要整体回滚,否则「校验」只是打印了一句话。
|
|
325
|
+
verify: () => {
|
|
326
|
+
const writtenMetadata = core.parseSkillMetadata(fs.readFileSync(resolvedSkillPath, 'utf8'));
|
|
327
|
+
if (writtenMetadata.name !== bundledMetadata.name) {
|
|
328
|
+
throw new Error('写入后的 SKILL.md 名称校验失败');
|
|
329
|
+
}
|
|
330
|
+
if (JSON.parse(fs.readFileSync(markerPath, 'utf8')).version !== SKILL_VERSION) {
|
|
331
|
+
throw new Error('写入后的 Skill 版本标记校验失败');
|
|
332
|
+
}
|
|
333
|
+
for (const asset of changedAssets) {
|
|
334
|
+
if (core.treeDigest(asset.target) !== core.treeDigest(asset.source)) {
|
|
335
|
+
throw new Error(`写入后的 Skill 资源目录校验失败:${asset.name}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (registryPreview.registryPath) {
|
|
339
|
+
core.assertRegistryEntryWritten({
|
|
340
|
+
registryPath: registryPreview.registryPath,
|
|
341
|
+
skillDir,
|
|
342
|
+
version: SKILL_VERSION,
|
|
343
|
+
name: bundledMetadata.name,
|
|
344
|
+
description: bundledMetadata.description
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
action: skillChanged || changedAssets.length > 0 ? 'updated' : 'registry_updated',
|
|
352
|
+
skillPath: resolvedSkillPath,
|
|
353
|
+
currentVersion,
|
|
354
|
+
targetVersion: SKILL_VERSION,
|
|
355
|
+
syncedAssets: changedAssets.map((asset) => asset.name),
|
|
356
|
+
registryPath: registryPreview.registryPath
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** 帮助文案是本仓的命令面,不进共享内核。 */
|
|
361
|
+
function printHelp() {
|
|
362
|
+
process.stdout.write(`${SKILL_DISPLAY_NAME} ${SKILL_VERSION}(Runtime ${RUNTIME_VERSION})
|
|
363
|
+
|
|
364
|
+
用法:
|
|
365
|
+
npx -y @yixinkj/${SKILL_ID}-cli@latest --skill-path <SKILL.md> <子命令> [选项]
|
|
366
|
+
|
|
367
|
+
通用:
|
|
368
|
+
--skill-path <PATH> 本次实际读取的 SKILL.md 绝对路径(每条命令都要带)
|
|
369
|
+
--version 显示版本
|
|
370
|
+
--help 显示本帮助
|
|
371
|
+
|
|
372
|
+
子命令:
|
|
373
|
+
fetch --detail-url <URL> 从询盘详情页只读抓取买家国家、需求卡片和买家原话
|
|
374
|
+
可加 --from-scan <FILE> 接住哨兵已取到的卡片,省掉一次开页
|
|
375
|
+
recall 召回档案、行为约束与长期记忆(--country/--product/--query/--budget)
|
|
376
|
+
validate --file <FILE> 校验三套开场白并渲染 Markdown(--inquiry-id 用于记账,勿省)
|
|
377
|
+
retain 写入一条记忆(--type fact|experience|directive|model,--source 必填)
|
|
378
|
+
outcome --id <ID> 为一条经验补记真实结果,使其可作为归纳证据
|
|
379
|
+
reflect 列出已有结果、尚未归纳的经验
|
|
380
|
+
memory list|get|update 查看与修改已有记忆
|
|
381
|
+
forget --id <ID> 永久删除一条记忆
|
|
382
|
+
profile show|save 读取或保存业务档案
|
|
383
|
+
bank list|create|use 管理记忆库分区(任意命令可用 --bank 临时指定)
|
|
384
|
+
self-check 本地结构与运行时自检
|
|
385
|
+
|
|
386
|
+
环境变量:
|
|
387
|
+
INQUIRY_OPENING_COACH_DATA_DIR 记忆库数据目录,默认 ~/.yixin/${SKILL_ID}/
|
|
388
|
+
|
|
389
|
+
本 Skill 抓取端只读:只读取询盘详情页,不发送任何消息、不修改任何页面状态。
|
|
390
|
+
`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 启动流程。
|
|
395
|
+
*
|
|
396
|
+
* 这段也没有进共享内核:四个仓的退出协议不同,文案里的动作名也不同。把它模板化会让
|
|
397
|
+
* 「改一句文案」变成「改共享库」,更糟的是会悄悄统一退出码——退出码是调用方(智能体)的协议。
|
|
398
|
+
*/
|
|
399
|
+
async function main() {
|
|
400
|
+
const rawArgs = process.argv.slice(2);
|
|
401
|
+
if (isHelpRequest(rawArgs)) {
|
|
402
|
+
printHelp();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const { skillPath, forwardedArgs } = core.extractSkillPath(rawArgs);
|
|
406
|
+
if (forwardedArgs.length === 1 && ['--version', '-V', '-v'].includes(forwardedArgs[0])) {
|
|
407
|
+
console.log(`${SKILL_ID} ${SKILL_VERSION} (runtime ${RUNTIME_VERSION})`);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
console.error(`[${SKILL_ID}] ${SKILL_DISPLAY_NAME} ${SKILL_VERSION}`);
|
|
412
|
+
const gate = resolveSkillGate(skillPath);
|
|
413
|
+
if (!gate.allowRun) {
|
|
414
|
+
if (gate.sync.action === 'local_newer') {
|
|
415
|
+
emitStatus(
|
|
416
|
+
'skill_version_conflict',
|
|
417
|
+
`当前 Skill 版本 ${gate.sync.currentVersion} 高于 npm 版本 ${SKILL_VERSION},已停止运行,未执行降级。`,
|
|
418
|
+
{ skill_path: gate.sync.skillPath }
|
|
419
|
+
);
|
|
420
|
+
process.exitCode = 1;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (gate.sync.action === 'skill_path_required') {
|
|
424
|
+
const found = gate.sync.skillPath;
|
|
425
|
+
emitStatus(
|
|
426
|
+
'skill_path_required',
|
|
427
|
+
found
|
|
428
|
+
? `缺少 --skill-path,本次未执行。${gate.sync.synced ? `已同步 Skill 到 ${SKILL_VERSION};` : ''}请带上参数重新运行:--skill-path "${found}"`
|
|
429
|
+
: '无法唯一确定当前智能体的 SKILL.md。请使用本次读取的 SKILL.md 绝对路径重新运行;本次未执行任何命令。',
|
|
430
|
+
{
|
|
431
|
+
skill_path: found || null,
|
|
432
|
+
candidates: gate.sync.candidates,
|
|
433
|
+
required_argument: '--skill-path'
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
process.exitCode = 1;
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
emitStatus(
|
|
440
|
+
'skill_updated',
|
|
441
|
+
`${SKILL_DISPLAY_NAME}已更新至 ${SKILL_VERSION}。请重新试一次,本次不执行任何命令。`,
|
|
442
|
+
{
|
|
443
|
+
skill_path: gate.sync.skillPath,
|
|
444
|
+
previous_version: gate.sync.currentVersion || null,
|
|
445
|
+
synced_assets: gate.sync.syncedAssets || [],
|
|
446
|
+
skills_registry: gate.sync.registryPath || null,
|
|
447
|
+
restart_required: true
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const runtimeOverride = process.env.INQUIRY_OPENING_COACH_RUNTIME_BINARY;
|
|
454
|
+
if (runtimeOverride) {
|
|
455
|
+
const binary = path.resolve(runtimeOverride);
|
|
456
|
+
if (!fs.existsSync(binary)) throw new Error(`开发运行时不存在:${binary}`);
|
|
457
|
+
const result = spawnSync(binary, forwardedArgs, {
|
|
458
|
+
stdio: 'inherit',
|
|
459
|
+
windowsHide: true,
|
|
460
|
+
env: process.env
|
|
461
|
+
});
|
|
462
|
+
if (result.error) throw result.error;
|
|
463
|
+
process.exitCode = result.status ?? 1;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const target = detectTarget();
|
|
468
|
+
const [sharedBridge] = await Promise.all([ensureSharedBridge(), ensureInstalled(target)]);
|
|
469
|
+
emitBrowserExtensionNotice(sharedBridge);
|
|
470
|
+
const result = spawnSync(path.join(BIN_DIR, target.binaries[0]), forwardedArgs, {
|
|
471
|
+
stdio: 'inherit',
|
|
472
|
+
windowsHide: true,
|
|
473
|
+
// 运行时在插件过旧时也要返回同一个目录,模型才能给出可执行的导入指引。
|
|
474
|
+
env: {
|
|
475
|
+
...process.env,
|
|
476
|
+
YIXIN_BRIDGE_BIN: sharedBridge.bridge_path,
|
|
477
|
+
...(sharedBridge.extension_path
|
|
478
|
+
? { YIXIN_EXTENSION_PATH: sharedBridge.extension_path }
|
|
479
|
+
: {})
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
if (result.error) throw result.error;
|
|
483
|
+
process.exitCode = result.status ?? 1;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// import.meta.url 必须由这里传:内核若用自己的 import.meta.url 去比,结果永远是 false,
|
|
487
|
+
// 启动器会变成「导入即返回、什么都不做」,而且不报错,只会静默失效。
|
|
488
|
+
if (core.isDirectExecution(import.meta.url)) {
|
|
489
|
+
main().catch((error) => fail(error.message));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 这几个与「我是谁」无关,内核版本逐字等价,直接转发即可,不必再包一层。
|
|
493
|
+
// browserExtensionNotice 的默认「未能自动加载」文案是完整版(先删旧插件、再按路径导入),
|
|
494
|
+
// 内核把它当默认值,因此这里不需要传 manualReloadMessage。
|
|
495
|
+
export {
|
|
496
|
+
browserExtensionNotice,
|
|
497
|
+
commitRuntimeInstall,
|
|
498
|
+
compareVersions,
|
|
499
|
+
extractSkillPath,
|
|
500
|
+
parseSkillMetadata,
|
|
501
|
+
treeDigest,
|
|
502
|
+
validateBridgeCapabilities
|
|
503
|
+
} from './launcher-core.js';
|
|
504
|
+
|
|
505
|
+
export {
|
|
506
|
+
BIN_DIR,
|
|
507
|
+
BUNDLED_SKILL_PATH,
|
|
508
|
+
RUNTIME_VERSION,
|
|
509
|
+
RUNTIME_ROOT,
|
|
510
|
+
SKILL_DISPLAY_NAME,
|
|
511
|
+
SKILL_ID,
|
|
512
|
+
SKILL_ROOT,
|
|
513
|
+
SKILL_VERSION,
|
|
514
|
+
VERSION_ROOT,
|
|
515
|
+
detectTarget,
|
|
516
|
+
discoverLegacySkillPaths,
|
|
517
|
+
downloadVerifiedRelease,
|
|
518
|
+
isHelpRequest,
|
|
519
|
+
isInstalled,
|
|
520
|
+
pruneOldRuntimeVersions,
|
|
521
|
+
reportPrunedRuntimeVersions,
|
|
522
|
+
releaseUrls,
|
|
523
|
+
resolveSkillGate,
|
|
524
|
+
runtimeManifestUrl,
|
|
525
|
+
runtimeManifestUrls,
|
|
526
|
+
syncSkillInstallation,
|
|
527
|
+
verifyReleaseArchive
|
|
528
|
+
};
|