kld-sdd 2.8.1 → 2.8.3
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/README.md +15 -0
- package/lib/init.js +3 -1
- package/package.json +4 -3
- package/skywalk-sdd/context-client.cjs +5 -2
- package/skywalk-sdd/kb-ingest.cjs +192 -0
- package/skywalk-sdd/kb-sync-identity.cjs +4 -42
- package/skywalk-sdd/kb-upload.cjs +84 -160
- package/skywalk-sdd/lib/kb-config.cjs +107 -0
- package/skywalk-sdd/lib/shared.cjs +7 -5
- package/skywalk-sdd/ontology/archive-package.cjs +1 -6
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +25 -185
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +13 -10
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +37 -287
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +34 -12
- package/templates/skills/kld-sdd/opsx-ontology-ingest/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +11 -7
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-1-prechange.md +1 -1
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-2-during.md +1 -1
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-4-explore.md +1 -1
- package/templates/skills/kld-sdd/opsx-ontology-query/phase-5-governance.md +1 -1
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +8 -3
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* kb-upload.cjs — 知识库上传脚本
|
|
6
6
|
*
|
|
7
7
|
* 支持两种上传模式:
|
|
8
|
-
* 1.
|
|
8
|
+
* 1. 文件夹上传:读取本地 Spec → 生成本体 JSON → 直接上传(无 ZIP、无 AI)
|
|
9
9
|
* 2. Zip 上传:直接上传已有 zip 包
|
|
10
10
|
*
|
|
11
11
|
* 用法:
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 历史产物(缺 entity-id / version-id):
|
|
18
18
|
* 自动旁路物化 spec-ontology.json,无 semantic_key 冲突时自动确认并发布为 current;
|
|
19
|
-
*
|
|
19
|
+
* --require-review 仅保存候选;明确审阅后用 --confirm-review 发布同一原文。
|
|
20
|
+
* 并发基线冲突整体拒绝,不覆盖团队 current。
|
|
20
21
|
*
|
|
21
22
|
* project_id 解析:
|
|
22
23
|
* 1. $SPEC_ROOT/skywalk-sdd/project-identity.json 的 project_id
|
|
23
|
-
* 2.
|
|
24
|
+
* 2. 缺少身份时自动创建稳定的 Spec 来源标识,不依赖上传库
|
|
25
|
+
* 3. 已有身份(含旧离线身份)原样保留,上传目标只由 uploadTarget 决定
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
28
|
const fs = require('fs');
|
|
@@ -37,16 +39,27 @@ const {
|
|
|
37
39
|
isKbStateHostDir,
|
|
38
40
|
uploadFile,
|
|
39
41
|
httpGetJson,
|
|
42
|
+
resolveKbParams,
|
|
40
43
|
} = require('./lib/shared.cjs');
|
|
41
44
|
|
|
42
45
|
function resolveUploadTarget(state, options = {}) {
|
|
43
46
|
if (Boolean(options.spaceId) !== Boolean(options.kbId)) throw new Error('必须同时指定 --space-id 和 --kb-id');
|
|
44
47
|
const targets = Array.isArray(state?.targets) ? state.targets : [];
|
|
48
|
+
const configured = state?.uploadTarget;
|
|
45
49
|
if (options.spaceId && options.kbId) {
|
|
46
|
-
|
|
50
|
+
const differs = Object.prototype.hasOwnProperty.call(state || {}, 'uploadTarget')
|
|
51
|
+
&& (!configured || configured.spaceId !== options.spaceId || configured.kbId !== options.kbId);
|
|
52
|
+
if (differs && !options.overrideUploadTarget) {
|
|
53
|
+
throw new Error('UPLOAD_TARGET_CONFLICT: 命令中的上传库与助手保存的上传位置不同;请更新旧 Skill,默认不传目标参数。确需临时改投时,显式增加 --override-upload-target');
|
|
54
|
+
}
|
|
55
|
+
const selected = targets.find(t => t.spaceId === options.spaceId && t.kbId === options.kbId);
|
|
56
|
+
if (options.overrideUploadTarget && !selected) throw new Error('临时上传目标必须在 kb-state.json 的 targets 中');
|
|
57
|
+
return selected || { spaceId: options.spaceId, kbId: options.kbId };
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
if (Object.prototype.hasOwnProperty.call(state || {}, 'uploadTarget') && configured == null) {
|
|
61
|
+
throw new Error('尚未选择 Spec 上传知识库(uploadTarget 已清空);请在助手中选择上传位置,不能用读取库代替');
|
|
62
|
+
}
|
|
50
63
|
if (configured != null) {
|
|
51
64
|
if (!configured.spaceId || !configured.kbId) {
|
|
52
65
|
throw new Error('kb-state.json 的 uploadTarget 不完整,请在 SDD 助手中重新选择 Spec 上传知识库');
|
|
@@ -106,9 +119,9 @@ function resolveKbStatePath(specRoot) {
|
|
|
106
119
|
function readArchiveCache(specRoot, changeName, context) {
|
|
107
120
|
const statePath = resolveKbStatePath(specRoot);
|
|
108
121
|
const state = readJsonSafe(statePath);
|
|
109
|
-
|
|
122
|
+
const local = require('./lib/kb-config.cjs').readReceipts(specRoot);
|
|
110
123
|
const key = cacheKey(changeName, context);
|
|
111
|
-
return key ? state
|
|
124
|
+
return key ? local.receipts?.[key] || local.legacyArchiveCache?.[key] || state?.archiveCache?.[key] || null : null;
|
|
112
125
|
}
|
|
113
126
|
|
|
114
127
|
/**
|
|
@@ -119,18 +132,14 @@ function readArchiveCache(specRoot, changeName, context) {
|
|
|
119
132
|
* @param {string} contentHash
|
|
120
133
|
*/
|
|
121
134
|
function updateKbStateCache(specRoot, changeName, archiveId, contentHash, context) {
|
|
122
|
-
const statePath = resolveKbStatePath(specRoot);
|
|
123
|
-
const state = readJsonSafe(statePath) || {};
|
|
124
|
-
if (!state.archiveCache) state.archiveCache = {};
|
|
125
135
|
const key = cacheKey(changeName, context);
|
|
126
136
|
if (!key) throw new Error('缓存必须包含 API、Space 和 KB');
|
|
127
|
-
|
|
137
|
+
require('./lib/kb-config.cjs').writeReceipt(specRoot, key, {
|
|
128
138
|
archive_id: archiveId,
|
|
129
139
|
content_hash: contentHash,
|
|
130
140
|
uploaded_at: new Date().toISOString(),
|
|
131
141
|
target: context,
|
|
132
|
-
};
|
|
133
|
-
writeJsonAtomic(statePath, state);
|
|
142
|
+
});
|
|
134
143
|
console.log(`✓ 已更新本地缓存: ${changeName} → ${archiveId.slice(0, 24)}...`);
|
|
135
144
|
}
|
|
136
145
|
|
|
@@ -164,19 +173,29 @@ function resolveSpecRoot(explicit) {
|
|
|
164
173
|
|
|
165
174
|
/**
|
|
166
175
|
* 解析 project_id
|
|
167
|
-
*
|
|
176
|
+
* 保留 Spec 来源标识;切换知识库不会改变实体及版本命名空间。
|
|
168
177
|
*/
|
|
169
|
-
function resolveProjectId(specRoot) {
|
|
178
|
+
function resolveProjectId(specRoot, target) {
|
|
170
179
|
const identityPath = path.join(specRoot, 'skywalk-sdd', 'project-identity.json');
|
|
180
|
+
if (!fs.existsSync(identityPath) && target) {
|
|
181
|
+
ensureDir(path.dirname(identityPath));
|
|
182
|
+
try {
|
|
183
|
+
fs.writeFileSync(identityPath, JSON.stringify({
|
|
184
|
+
schema_version: 'kld-sdd-project-identity/v1',
|
|
185
|
+
project_id: 'urn:kld:sdd:project:' + require('crypto').randomBytes(16).toString('hex'),
|
|
186
|
+
created_at: new Date().toISOString(),
|
|
187
|
+
}, null, 2) + '\n', { flag: 'wx' });
|
|
188
|
+
} catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
189
|
+
}
|
|
171
190
|
if (fs.existsSync(identityPath)) {
|
|
172
191
|
const identity = readJson(identityPath);
|
|
173
|
-
if (identity.project_id) {
|
|
192
|
+
if (typeof identity?.project_id === 'string' && identity.project_id.trim()) {
|
|
174
193
|
return identity.project_id;
|
|
175
194
|
}
|
|
176
195
|
}
|
|
177
196
|
throw new Error(
|
|
178
197
|
'无法确定 project_id。project-identity.json 不存在或无 project_id 字段。\n' +
|
|
179
|
-
'
|
|
198
|
+
'请检查该文件;上传目标在知识库配置中单独选择。'
|
|
180
199
|
);
|
|
181
200
|
}
|
|
182
201
|
|
|
@@ -186,9 +205,9 @@ function resolveProjectId(specRoot) {
|
|
|
186
205
|
* 位置校验:只读 `<specRoot>/kb-state.json`(specRoot 的 spec 根身份已由
|
|
187
206
|
* resolveSpecRoot 在入口校验)。
|
|
188
207
|
*/
|
|
189
|
-
function readKbState(specRoot) {
|
|
208
|
+
function readKbState(specRoot, env = process.env) {
|
|
190
209
|
const statePath = resolveKbStatePath(specRoot);
|
|
191
|
-
if (!fs.existsSync(statePath)) {
|
|
210
|
+
if (!fs.existsSync(statePath) && !fs.existsSync(path.join(specRoot, 'kb.config.json'))) {
|
|
192
211
|
throw new Error(
|
|
193
212
|
'KB 配置不存在(kb-state.json 缺失)。\n' +
|
|
194
213
|
`期望位置: ${statePath}\n` +
|
|
@@ -196,7 +215,14 @@ function readKbState(specRoot) {
|
|
|
196
215
|
);
|
|
197
216
|
}
|
|
198
217
|
try {
|
|
199
|
-
|
|
218
|
+
const loaded = require('./lib/kb-config.cjs').readProjectConfig(specRoot);
|
|
219
|
+
const state = loaded.config;
|
|
220
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) throw new Error('配置内容必须是 JSON 对象');
|
|
221
|
+
if (env.SDD_KB_STATE_FILE && path.resolve(env.SDD_KB_STATE_FILE) !== statePath) {
|
|
222
|
+
console.warn('⚠️ SDD_KB_STATE_FILE 已忽略;上传统一读取所选 Spec 根目录的 kb-state.json');
|
|
223
|
+
}
|
|
224
|
+
const params = resolveKbParams(state, env);
|
|
225
|
+
return { ...state, api: params.apiBase, apiKey: params.token };
|
|
200
226
|
} catch (e) {
|
|
201
227
|
throw new Error(
|
|
202
228
|
`KB 配置解析失败 (${statePath}): ${e.message}\n` +
|
|
@@ -205,6 +231,16 @@ function readKbState(specRoot) {
|
|
|
205
231
|
}
|
|
206
232
|
}
|
|
207
233
|
|
|
234
|
+
// Upload and identity synchronization share this entry; neither may reinterpret read targets.
|
|
235
|
+
function prepareUploadConfig(specRoot, options = {}) {
|
|
236
|
+
const state = readKbState(specRoot);
|
|
237
|
+
if (!state.apiKey) throw new Error('KB 配置中无 apiKey,请在助手中登录知识库或运行 /opsx-kb-config');
|
|
238
|
+
const target = resolveUploadTarget(state, options);
|
|
239
|
+
const projectId = resolveProjectId(specRoot, options.readOnlyIdentity ? undefined : target);
|
|
240
|
+
preCheckProjectId(projectId, state, target);
|
|
241
|
+
return { state, target, projectId };
|
|
242
|
+
}
|
|
243
|
+
|
|
208
244
|
/**
|
|
209
245
|
* 校验文件夹必需文件
|
|
210
246
|
*/
|
|
@@ -230,26 +266,15 @@ function validateRequiredFiles(folderPath, minimumLevel) {
|
|
|
230
266
|
}
|
|
231
267
|
|
|
232
268
|
/**
|
|
233
|
-
*
|
|
269
|
+
* 兼容旧调用:只检查来源标识和有效目标,不限制来源与目标空间一致。
|
|
234
270
|
*/
|
|
235
271
|
function preCheckProjectId(projectId, state, selectedTarget) {
|
|
236
272
|
if (!state.targets || !state.targets[0]) {
|
|
237
273
|
throw new Error('KB 配置中无 targets,请运行 /opsx-kb-config 配置知识库。');
|
|
238
274
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (projectId !== spaceKey) {
|
|
245
|
-
throw new Error(
|
|
246
|
-
`PROJECT_SPACE_MISMATCH 预检失败:\n` +
|
|
247
|
-
` project_id (project-identity.json): ${projectId}\n` +
|
|
248
|
-
` spaceKey (kb-state.json): ${spaceKey}\n` +
|
|
249
|
-
`请运行 /opsx-kb-config 更新配置。`
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
console.log(`✓ project_id 预检通过: ${projectId} === ${spaceKey}`);
|
|
275
|
+
if (typeof projectId !== 'string' || !projectId.trim()) throw new Error('Spec 来源标识为空,请修复 project-identity.json');
|
|
276
|
+
const target = selectedTarget || resolveUploadTarget(state);
|
|
277
|
+
if (!target.spaceId || !target.kbId) throw new Error('请选择有效的上传知识库');
|
|
253
278
|
}
|
|
254
279
|
|
|
255
280
|
/**
|
|
@@ -446,7 +471,23 @@ async function uploadZip(zipPath, state, options) {
|
|
|
446
471
|
async function main() {
|
|
447
472
|
const args = parseArgs(process.argv.slice(2));
|
|
448
473
|
|
|
449
|
-
if (
|
|
474
|
+
if (args['check-config']) {
|
|
475
|
+
try {
|
|
476
|
+
const specRoot = resolveSpecRoot(args.project);
|
|
477
|
+
const state = readKbState(specRoot);
|
|
478
|
+
if (!state.apiKey) throw new Error('尚未配置知识库 API Key');
|
|
479
|
+
const target = resolveUploadTarget(state);
|
|
480
|
+
const identityPath = path.join(specRoot, 'skywalk-sdd/project-identity.json');
|
|
481
|
+
const identity = fs.existsSync(identityPath) ? readJson(identityPath) : null;
|
|
482
|
+
if (fs.existsSync(identityPath) && (!identity || typeof identity.project_id !== 'string' || !identity.project_id.trim())) throw new Error('项目身份文件缺少 project_id,已保留原文件,请先修复');
|
|
483
|
+
if (identity?.project_id) preCheckProjectId(identity.project_id, state, target);
|
|
484
|
+
console.log(JSON.stringify({ configFile: resolveKbStatePath(specRoot), api: state.api,
|
|
485
|
+
uploadTarget: { spaceId: target.spaceId, kbId: target.kbId, spaceKey: target.spaceKey, spaceName: target.spaceName, kbName: target.kbName }, projectId: identity?.project_id || null,
|
|
486
|
+
identityWillBeCreated: !identity, identityWillBeBound: false }, null, 2));
|
|
487
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (!args.folder && !args.package && !args.change) {
|
|
450
491
|
console.error('用法:');
|
|
451
492
|
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]');
|
|
452
493
|
console.error(' node skywalk-sdd/kb-upload.cjs --package=<zip-path>');
|
|
@@ -483,6 +524,7 @@ async function main() {
|
|
|
483
524
|
const result = await uploadZip(zipPath, state, {
|
|
484
525
|
spaceId: args['space-id'],
|
|
485
526
|
kbId: args['kb-id'],
|
|
527
|
+
overrideUploadTarget: Boolean(args['override-upload-target']),
|
|
486
528
|
});
|
|
487
529
|
|
|
488
530
|
console.log('\n── 上传结果 ──');
|
|
@@ -550,130 +592,11 @@ async function main() {
|
|
|
550
592
|
return;
|
|
551
593
|
}
|
|
552
594
|
|
|
553
|
-
//
|
|
554
|
-
const projectId = resolveProjectId(specRoot);
|
|
555
|
-
console.log(`✓ project_id: ${projectId}`);
|
|
556
|
-
|
|
557
|
-
// 3. 读取 KB 配置 + 预检
|
|
558
|
-
const state = readKbState(specRoot);
|
|
559
|
-
if (!state.apiKey) {
|
|
560
|
-
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
561
|
-
}
|
|
562
|
-
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
563
|
-
preCheckProjectId(projectId, state, target);
|
|
564
|
-
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
565
|
-
|
|
566
|
-
// 5. 构建 Archive Package
|
|
567
|
-
console.log('');
|
|
568
|
-
const { zipPath, manifest, stagingDir, requiresReview, legacyStatus } = buildPackageFromFolder(specRoot, changeName, {
|
|
569
|
-
reason: args.reason,
|
|
570
|
-
requireReview: Boolean(args['require-review']),
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
try {
|
|
574
|
-
// Local receipts are informational only. Always verify the selected destination remotely.
|
|
575
|
-
const apiUrl = (state.api || 'http://localhost:8090/api').replace(/\/+$/, '');
|
|
576
|
-
|
|
577
|
-
// === Step 5.5: KB 预检(网络级幂等确认)===
|
|
578
|
-
const archiveId = manifest.archive_id;
|
|
579
|
-
const contentHash = manifest.content_hash;
|
|
580
|
-
console.log(`→ Pre-check: archive_id=${archiveId}`);
|
|
581
|
-
console.log(` content_hash=${contentHash}`);
|
|
582
|
-
|
|
583
|
-
const checkUrl = `${apiUrl}/v1/spaces/${target.spaceId}/knowledge-bases/${target.kbId}/ingestions/check`
|
|
584
|
-
+ `?archiveId=${encodeURIComponent(archiveId)}`
|
|
585
|
-
+ `&contentHash=${encodeURIComponent(contentHash)}`;
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
const checkResult = await httpGetJson(checkUrl, state.apiKey);
|
|
589
|
-
|
|
590
|
-
if (checkResult?.data?.idempotent && checkResult.data.publicationStatus === 'published') {
|
|
591
|
-
console.log('\n✓ KB 中已存在相同 archive(幂等命中),跳过上传');
|
|
592
|
-
console.log(` snapshot_id: ${checkResult.data.snapshotId}`);
|
|
593
|
-
updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
|
|
594
|
-
console.log(` publication: ${checkResult.data.publicationStatus}; projection: ${checkResult.data.projectionStatus}`);
|
|
595
|
-
if (['failed', 'degraded'].includes(checkResult.data.projectionStatus)) console.log('⚠️ 检索准备尚未完成,请在知识库概览重试该任务的投影');
|
|
596
|
-
console.log('\n✓ 归档已存在(检索状态以上述回执为准)');
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (checkResult && checkResult.data && checkResult.data.exists && !checkResult.data.idempotent) {
|
|
601
|
-
throw new Error(
|
|
602
|
-
`ARCHIVE_IMMUTABILITY_VIOLATION: archive_id 已存在但 content_hash 不同\n` +
|
|
603
|
-
` archive_id: ${archiveId}\n` +
|
|
604
|
-
` 如果内容确实变更,请确认实体身份块已正确更新`
|
|
605
|
-
);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
console.log('✓ Pre-check 通过(KB 中无此 archive,继续上传)');
|
|
609
|
-
} catch (checkErr) {
|
|
610
|
-
// 降级:KB 后端未部署 /check 接口(404)→ 继续上传
|
|
611
|
-
if (checkErr.message && checkErr.message.includes('HTTP 404')) {
|
|
612
|
-
console.log('⚠️ KB 后端未部署 /check 接口,降级为直接上传');
|
|
613
|
-
} else {
|
|
614
|
-
throw checkErr;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
// 6. 上传
|
|
619
|
-
console.log('');
|
|
620
|
-
const result = await uploadZip(zipPath, state, {
|
|
621
|
-
spaceId: target.spaceId,
|
|
622
|
-
kbId: target.kbId,
|
|
623
|
-
allowNeedsReview: requiresReview,
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
console.log('\n── 上传结果 ──');
|
|
627
|
-
console.log(JSON.stringify(result, null, 2));
|
|
628
|
-
|
|
629
|
-
// 7. 输出 manifest 摘要
|
|
630
|
-
console.log('\n── Archive Package 摘要 ──');
|
|
631
|
-
console.log(` archive_id: ${manifest.archive_id}`);
|
|
632
|
-
console.log(` project_id: ${manifest.project_id}`);
|
|
633
|
-
console.log(` change_id: ${manifest.change_id}`);
|
|
634
|
-
console.log(` files: ${manifest.files.length}`);
|
|
635
|
-
|
|
636
|
-
const publicationStatus = result?.data?.report?.publication_status
|
|
637
|
-
|| result?.report?.publication_status
|
|
638
|
-
|| '';
|
|
639
|
-
if (publicationStatus === 'needs_review') {
|
|
640
|
-
console.log('\n⚠️ 历史 Spec 已安全保存为候选,尚未进入 current 召回');
|
|
641
|
-
console.log(` → 审核 ${path.join(folderPath, 'ontology', 'spec-ontology.json')}`);
|
|
642
|
-
if (legacyStatus === 'needs_review') {
|
|
643
|
-
console.log(' → 正文没有足够的结构化事实,只能作为证据保留');
|
|
644
|
-
} else {
|
|
645
|
-
console.log(' → 运行 approve-spec-ontology 后再次上传即可发布;不带 --require-review 上传则自动确认');
|
|
646
|
-
}
|
|
647
|
-
} else {
|
|
648
|
-
// Published commit has been verified; a receipt does not imply projection completion.
|
|
649
|
-
updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// === 投影状态提示 ===
|
|
653
|
-
// 上传成功后,KB 后端异步执行 vector embedding + AGE graph 投影。
|
|
654
|
-
// 正常 SDD 流程中,Spec→Design 等阶段间有用户手动触发的时间间隔,投影通常在此期间完成。
|
|
655
|
-
const projStatus = result?.data?.projectionStatus || result?.projectionStatus || '';
|
|
656
|
-
const jobStatus = result?.data?.status || result?.status || '';
|
|
657
|
-
|
|
658
|
-
if (projStatus === 'complete') {
|
|
659
|
-
console.log('\n✓ 投影已完成,向量/图检索可用');
|
|
660
|
-
} else if (projStatus === 'degraded') {
|
|
661
|
-
console.log('\n⚠️ 投影完成但有降级(部分通道不可用,召回结果会标记降级)');
|
|
662
|
-
} else if (projStatus === 'disabled') {
|
|
663
|
-
console.log('\nℹ️ 投影已禁用(仅可使用当前已就绪的检索通道)');
|
|
664
|
-
} else if (projStatus && projStatus !== 'complete') {
|
|
665
|
-
console.log(`\nℹ️ 投影进行中 (${projStatus}),向量/图检索暂不可用`);
|
|
666
|
-
console.log(' → 正常 SDD 流程中阶段间间隔通常足够投影完成');
|
|
667
|
-
console.log(' → 如后续查询 degraded,可用 `context-client.cjs --check-only` 确认 KB 可用性');
|
|
668
|
-
} else if (jobStatus === 'succeeded') {
|
|
669
|
-
console.log('\n✓ 入库成功');
|
|
670
|
-
}
|
|
595
|
+
// Direct JSON transport reads the existing documents; ZIP remains for --package compatibility.
|
|
671
596
|
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
console.log('\n✓ 临时文件已清理');
|
|
676
|
-
}
|
|
597
|
+
const result = await require('./kb-ingest.cjs').uploadChanges(specRoot, [{ key: changeName }], { spaceId: args['space-id'], kbId: args['kb-id'], overrideUploadTarget: Boolean(args['override-upload-target']), requireReview: Boolean(args['require-review']), confirmReview: Boolean(args['confirm-review']) });
|
|
598
|
+
console.log(JSON.stringify(result, null, 2));
|
|
599
|
+
if (result.items.some(item => item.status === 'failed')) { for (const item of result.items.filter(i => i.status === 'failed')) console.error(item.message); process.exitCode = 1; }
|
|
677
600
|
|
|
678
601
|
} catch (err) {
|
|
679
602
|
console.error(`\n❌ ${err.message}`);
|
|
@@ -681,5 +604,6 @@ async function main() {
|
|
|
681
604
|
}
|
|
682
605
|
}
|
|
683
606
|
|
|
607
|
+
module.exports = { resolveUploadTarget, prepareUploadConfig, readKbState, readArchiveCache, updateKbStateCache, assertIngestionResult, uploadZip, buildPackageFromFolder, validateRequiredFiles };
|
|
608
|
+
|
|
684
609
|
if (require.main === module) main();
|
|
685
|
-
module.exports = { resolveUploadTarget, readArchiveCache, updateKbStateCache, assertIngestionResult, uploadZip, buildPackageFromFolder, validateRequiredFiles };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// One resolver for tray, Skills, retrieval, uploads and reporting. Never export a
|
|
3
|
+
// personal file wholesale as team configuration.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const PUBLIC_KEYS = ['api', 'targets', 'uploadTarget'];
|
|
8
|
+
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
9
|
+
function endpoint(value) {
|
|
10
|
+
const url = new URL(value);
|
|
11
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('知识库服务地址无效');
|
|
12
|
+
return url.toString().replace(/\/$/, '').replace(/\/api$/, '') + '/api';
|
|
13
|
+
}
|
|
14
|
+
function objectFile(file) {
|
|
15
|
+
if (!fs.existsSync(file)) return null;
|
|
16
|
+
if (fs.lstatSync(file).isSymbolicLink()) throw new Error('配置文件不能是符号链接');
|
|
17
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
|
|
18
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('配置必须是 JSON 对象');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function paths(root) {
|
|
22
|
+
return { team: path.join(root, 'kb.config.json'), personal: path.join(root, 'kb-state.json'), state: path.join(root, 'skywalk-sdd/state/ingestions.json') };
|
|
23
|
+
}
|
|
24
|
+
function unionTargets(a, b) {
|
|
25
|
+
const result = [];
|
|
26
|
+
for (const t of [...(a || []), ...(b || [])]) {
|
|
27
|
+
if (!t || typeof t.spaceId !== 'string' || typeof t.kbId !== 'string' || !t.spaceId || !t.kbId) throw new Error('知识库目标必须包含空间和知识库');
|
|
28
|
+
if (!result.some(x => x.spaceId === t.spaceId && x.kbId === t.kbId)) result.push(t);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function readProjectConfig(root) {
|
|
33
|
+
const files = paths(root), personal = objectFile(files.personal) || {}, team = objectFile(files.team);
|
|
34
|
+
const revision = hash([files.team, files.personal].map(f => fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '').join('\0'));
|
|
35
|
+
let config = { ...personal }, warnings = [];
|
|
36
|
+
if (team) {
|
|
37
|
+
if (team.schemaVersion !== 1) throw new Error('团队知识库配置版本不兼容,请升级 kld-sdd');
|
|
38
|
+
if (Object.keys(team).some(k => !['schemaVersion', ...PUBLIC_KEYS].includes(k))) throw new Error('团队知识库配置包含非公共字段,请移到个人配置');
|
|
39
|
+
if (!Array.isArray(team.targets) || !Object.hasOwn(team, 'uploadTarget')) throw new Error('团队配置缺少读取范围或上传位置');
|
|
40
|
+
config = { ...personal, api: endpoint(team.api), targets: unionTargets(team.targets, personal.personalReadTargets), uploadTarget: team.uploadTarget };
|
|
41
|
+
}
|
|
42
|
+
const credentialApi = personal.credentialForApi || personal.api;
|
|
43
|
+
if (personal.apiKey && config.api && ((!credentialApi && team) || (credentialApi && credentialApi !== config.api && endpoint(credentialApi) !== endpoint(config.api)))) {
|
|
44
|
+
config.apiKey = ''; config.credentialMismatch = true;
|
|
45
|
+
warnings.push('知识库服务地址已变化,请重新登录;原 Key 已保留在本机');
|
|
46
|
+
}
|
|
47
|
+
// A changed shared server must not inherit a process-wide credential either.
|
|
48
|
+
if (team && !config.apiKey) config.credentialMismatch = true;
|
|
49
|
+
return { config, personal, team, files, revision, warnings, path: fs.existsSync(files.personal) || team ? files.personal : null };
|
|
50
|
+
}
|
|
51
|
+
function atomic(file, value) {
|
|
52
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
53
|
+
if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) throw new Error('配置文件不能是符号链接');
|
|
54
|
+
const temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
|
|
55
|
+
try { fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', { mode: 0o600, flag: 'wx' }); fs.renameSync(temp, file); }
|
|
56
|
+
finally { fs.rmSync(temp, { force: true }); }
|
|
57
|
+
}
|
|
58
|
+
function saveProjectConfig(root, config, options = {}) {
|
|
59
|
+
return require('../ontology/change-lock.cjs').withChangeLock(root, 'kb-config', {}, () => {
|
|
60
|
+
const before = readProjectConfig(root);
|
|
61
|
+
if (options.revision != null && options.revision !== before.revision) throw new Error('配置已被其他程序修改,请重新读取');
|
|
62
|
+
const api = endpoint(config.api);
|
|
63
|
+
protectPersonal(root);
|
|
64
|
+
const apiKey = config.credentialMismatch && !config.apiKey ? before.personal.apiKey : config.apiKey;
|
|
65
|
+
const sameKey = apiKey && apiKey === before.personal.apiKey;
|
|
66
|
+
const credentialApi = sameKey && !options.credentialVerified ? (before.personal.credentialForApi || before.personal.api || api) : api;
|
|
67
|
+
let personal = { ...before.personal, ...config, ...(apiKey !== undefined ? { apiKey } : {}), api, credentialForApi: credentialApi };
|
|
68
|
+
if (options.team === true || before.team) {
|
|
69
|
+
const targets = unionTargets(config.targets, []);
|
|
70
|
+
const selected = config.uploadTarget || null;
|
|
71
|
+
if (selected && !targets.some(t => t.spaceId === selected.spaceId && t.kbId === selected.kbId)) throw new Error('上传位置必须在所选知识库中');
|
|
72
|
+
if (options.team === true) {
|
|
73
|
+
const team = { schemaVersion: 1, api, targets: targets.map(t => Object.fromEntries(['spaceId', 'kbId', 'spaceKey', 'spaceName', 'kbName'].filter(k => typeof t[k] === 'string').map(k => [k, t[k]]))), uploadTarget: selected && { spaceId: selected.spaceId, kbId: selected.kbId } };
|
|
74
|
+
atomic(before.files.team, team);
|
|
75
|
+
} else {
|
|
76
|
+
if (api !== endpoint(before.team.api) || JSON.stringify(selected) !== JSON.stringify(before.team.uploadTarget)) throw new Error('修改团队地址或上传位置,请选择「团队共享」保存');
|
|
77
|
+
}
|
|
78
|
+
const defaults = options.team === true ? targets : before.team.targets;
|
|
79
|
+
personal.personalReadTargets = targets.filter(t => !defaults.some(x => x.spaceId === t.spaceId && x.kbId === t.kbId));
|
|
80
|
+
delete personal.targets; delete personal.uploadTarget;
|
|
81
|
+
}
|
|
82
|
+
delete personal.credentialMismatch;
|
|
83
|
+
if (personal.archiveCache) {
|
|
84
|
+
const state = objectFile(before.files.state) || { schemaVersion: 1, receipts: {} };
|
|
85
|
+
state.legacyArchiveCache = { ...personal.archiveCache, ...state.legacyArchiveCache };
|
|
86
|
+
atomic(before.files.state, state); delete personal.archiveCache;
|
|
87
|
+
}
|
|
88
|
+
protectPersonal(root);
|
|
89
|
+
atomic(before.files.personal, personal);
|
|
90
|
+
return readProjectConfig(root);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function readReceipts(root) { return objectFile(paths(root).state) || { schemaVersion: 1, receipts: {} }; }
|
|
94
|
+
function writeReceipt(root, key, value) {
|
|
95
|
+
return require('../ontology/change-lock.cjs').withChangeLock(root, 'kb-config', {}, () => {
|
|
96
|
+
const state = readReceipts(root); state.receipts ||= {}; state.receipts[key] = value;
|
|
97
|
+
protectPersonal(root); atomic(paths(root).state, state); return value;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function protectPersonal(root) {
|
|
101
|
+
const file = path.join(root, '.gitignore');
|
|
102
|
+
if (fs.existsSync(file) && fs.lstatSync(file).isSymbolicLink()) throw new Error('.gitignore 不能是符号链接');
|
|
103
|
+
const old = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
104
|
+
const missing = ['/kb-state.json', '/skywalk-sdd/state/'].filter(rule => !old.split(/\r?\n/).includes(rule));
|
|
105
|
+
if (missing.length) fs.writeFileSync(file, old + (old.endsWith('\n') || !old ? '' : '\n') + missing.join('\n') + '\n');
|
|
106
|
+
}
|
|
107
|
+
module.exports = { endpoint, paths, readProjectConfig, saveProjectConfig, readReceipts, writeReceipt, atomic, unionTargets };
|
|
@@ -352,12 +352,13 @@ function loadKbState(anchorDir = __dirname, env = process.env) {
|
|
|
352
352
|
let current = start;
|
|
353
353
|
for (let level = 0; level < MAX_KB_STATE_LEVELS; level += 1) {
|
|
354
354
|
const candidate = path.join(current, KB_STATE_FILENAME);
|
|
355
|
-
if (fs.existsSync(candidate) && isKbStateHostDir(current)) {
|
|
355
|
+
if ((fs.existsSync(candidate) || fs.existsSync(path.join(current, 'kb.config.json'))) && isKbStateHostDir(current)) {
|
|
356
356
|
try {
|
|
357
|
-
|
|
358
|
-
return { config, path: candidate, warnings };
|
|
357
|
+
return require('./kb-config.cjs').readProjectConfig(current);
|
|
359
358
|
} catch (e) {
|
|
360
359
|
warnings.push(`kb-state.json 解析失败 (${candidate}): ${e.message}`);
|
|
360
|
+
// A broken configuration belongs to this Spec. Never borrow another project's credentials.
|
|
361
|
+
return { config: null, path: candidate, warnings };
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
if (current === fileRoot) break;
|
|
@@ -391,7 +392,7 @@ function normalizeProjectName(value) {
|
|
|
391
392
|
*/
|
|
392
393
|
function resolveKbParams(stateConfig = {}, env = process.env) {
|
|
393
394
|
const apiBase = stateConfig.api || env.ENGINEERING_KB_API || 'http://localhost:8090/api';
|
|
394
|
-
const token = stateConfig.apiKey || env.ENGINEERING_KB_TOKEN || '';
|
|
395
|
+
const token = stateConfig.credentialMismatch ? '' : stateConfig.apiKey || env.ENGINEERING_KB_TOKEN || '';
|
|
395
396
|
const spaceId = stateConfig.targets && stateConfig.targets.length > 0
|
|
396
397
|
? stateConfig.targets[0].spaceId : '';
|
|
397
398
|
const kbId = stateConfig.targets && stateConfig.targets.length > 0
|
|
@@ -523,7 +524,8 @@ function setKbProjectName(specRoot, projectName, options = {}) {
|
|
|
523
524
|
error.code = 'E_PROJECT_NAME_INVALID';
|
|
524
525
|
throw error;
|
|
525
526
|
}
|
|
526
|
-
const
|
|
527
|
+
const personal = require('./kb-config.cjs').readProjectConfig(root).personal;
|
|
528
|
+
const next = { ...personal, projectName: normalized };
|
|
527
529
|
writeJsonAtomic(statePath, next);
|
|
528
530
|
return { ok: true, path: statePath, projectName: next.projectName };
|
|
529
531
|
}
|
|
@@ -57,16 +57,11 @@ function ensureProjectIdentity(projectRoot) {
|
|
|
57
57
|
}
|
|
58
58
|
return existing;
|
|
59
59
|
}
|
|
60
|
-
//
|
|
61
|
-
// 此时 project_id 为随机值,KB 入库时会因 project_id !== spaceKey 而失败
|
|
62
|
-
console.warn('⚠️ project-identity.json 不存在 — 未通过 opsx-kb-config 配置 KB。');
|
|
63
|
-
console.warn('⚠️ 将生成临时 project_id,KB 入库时需确保与 Space spaceKey 一致。');
|
|
64
|
-
console.warn('⚠️ 建议运行 /opsx-kb-config 配置 KB 后更新 project_id 为 Space spaceKey。');
|
|
60
|
+
// Stable source identity is independent of where the package is uploaded.
|
|
65
61
|
const identity = {
|
|
66
62
|
schema_version: PROJECT_IDENTITY_SCHEMA_VERSION,
|
|
67
63
|
project_id: `urn:kld:sdd:project:${generateUuidV7()}`,
|
|
68
64
|
created_at: new Date().toISOString(),
|
|
69
|
-
_warning: 'UNCONFIGURED — 请运行 /opsx-kb-config 配置 KB 后更新 project_id 为 Space spaceKey',
|
|
70
65
|
};
|
|
71
66
|
writeJsonAtomic(identityPath, identity);
|
|
72
67
|
return identity;
|