kld-sdd 2.8.1 → 2.8.2
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 +191 -0
- package/skywalk-sdd/kb-sync-identity.cjs +4 -42
- package/skywalk-sdd/kb-upload.cjs +94 -144
- package/skywalk-sdd/lib/kb-config.cjs +107 -0
- package/skywalk-sdd/lib/shared.cjs +7 -5
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +25 -185
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +7 -2
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +37 -287
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +30 -10
- 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. 未配置的首次身份 → 使用已选上传库的 spaceKey 建立归属
|
|
25
|
+
* 3. 缺少空间元数据或已有绑定不符 → 停止并提示修正配置
|
|
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,13 +173,35 @@ function resolveSpecRoot(explicit) {
|
|
|
164
173
|
|
|
165
174
|
/**
|
|
166
175
|
* 解析 project_id
|
|
167
|
-
*
|
|
176
|
+
* 保留团队绑定;仅缺失或明确 UNCONFIGURED 的身份可从已选上传目标首次建立。
|
|
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) && typeof target?.spaceKey === 'string' && target.spaceKey.trim()) {
|
|
181
|
+
ensureDir(path.dirname(identityPath));
|
|
182
|
+
try {
|
|
183
|
+
fs.writeFileSync(identityPath, JSON.stringify({
|
|
184
|
+
schema_version: 'kld-sdd-project-identity/v1',
|
|
185
|
+
project_id: target.spaceKey.trim(),
|
|
186
|
+
kb_space_id: target.spaceId,
|
|
187
|
+
kb_space_name: target.spaceName || '',
|
|
188
|
+
created_at: new Date().toISOString(),
|
|
189
|
+
}, null, 2) + '\n', { flag: 'wx' });
|
|
190
|
+
} catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
191
|
+
}
|
|
171
192
|
if (fs.existsSync(identityPath)) {
|
|
172
193
|
const identity = readJson(identityPath);
|
|
173
|
-
|
|
194
|
+
const provisional = !identity?.kb_space_id && typeof identity?._warning === 'string'
|
|
195
|
+
&& identity._warning.startsWith('UNCONFIGURED') && /^urn:kld:sdd:project:/.test(identity.project_id || '');
|
|
196
|
+
if (provisional && typeof target?.spaceKey === 'string' && target.spaceKey.trim()) {
|
|
197
|
+
const next = { ...identity, previous_unconfigured_project_id: identity.project_id,
|
|
198
|
+
project_id: target.spaceKey.trim(), kb_space_id: target.spaceId, kb_space_name: target.spaceName || '',
|
|
199
|
+
configured_at: new Date().toISOString() };
|
|
200
|
+
delete next._warning;
|
|
201
|
+
writeJsonAtomic(identityPath, next);
|
|
202
|
+
return next.project_id;
|
|
203
|
+
}
|
|
204
|
+
if (typeof identity?.project_id === 'string' && identity.project_id.trim()) {
|
|
174
205
|
return identity.project_id;
|
|
175
206
|
}
|
|
176
207
|
}
|
|
@@ -186,9 +217,9 @@ function resolveProjectId(specRoot) {
|
|
|
186
217
|
* 位置校验:只读 `<specRoot>/kb-state.json`(specRoot 的 spec 根身份已由
|
|
187
218
|
* resolveSpecRoot 在入口校验)。
|
|
188
219
|
*/
|
|
189
|
-
function readKbState(specRoot) {
|
|
220
|
+
function readKbState(specRoot, env = process.env) {
|
|
190
221
|
const statePath = resolveKbStatePath(specRoot);
|
|
191
|
-
if (!fs.existsSync(statePath)) {
|
|
222
|
+
if (!fs.existsSync(statePath) && !fs.existsSync(path.join(specRoot, 'kb.config.json'))) {
|
|
192
223
|
throw new Error(
|
|
193
224
|
'KB 配置不存在(kb-state.json 缺失)。\n' +
|
|
194
225
|
`期望位置: ${statePath}\n` +
|
|
@@ -196,7 +227,14 @@ function readKbState(specRoot) {
|
|
|
196
227
|
);
|
|
197
228
|
}
|
|
198
229
|
try {
|
|
199
|
-
|
|
230
|
+
const loaded = require('./lib/kb-config.cjs').readProjectConfig(specRoot);
|
|
231
|
+
const state = loaded.config;
|
|
232
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) throw new Error('配置内容必须是 JSON 对象');
|
|
233
|
+
if (env.SDD_KB_STATE_FILE && path.resolve(env.SDD_KB_STATE_FILE) !== statePath) {
|
|
234
|
+
console.warn('⚠️ SDD_KB_STATE_FILE 已忽略;上传统一读取所选 Spec 根目录的 kb-state.json');
|
|
235
|
+
}
|
|
236
|
+
const params = resolveKbParams(state, env);
|
|
237
|
+
return { ...state, api: params.apiBase, apiKey: params.token };
|
|
200
238
|
} catch (e) {
|
|
201
239
|
throw new Error(
|
|
202
240
|
`KB 配置解析失败 (${statePath}): ${e.message}\n` +
|
|
@@ -205,6 +243,16 @@ function readKbState(specRoot) {
|
|
|
205
243
|
}
|
|
206
244
|
}
|
|
207
245
|
|
|
246
|
+
// Upload and identity synchronization share this entry; neither may reinterpret read targets.
|
|
247
|
+
function prepareUploadConfig(specRoot, options = {}) {
|
|
248
|
+
const state = readKbState(specRoot);
|
|
249
|
+
if (!state.apiKey) throw new Error('KB 配置中无 apiKey,请在助手中登录知识库或运行 /opsx-kb-config');
|
|
250
|
+
const target = resolveUploadTarget(state, options);
|
|
251
|
+
const projectId = resolveProjectId(specRoot, options.readOnlyIdentity ? undefined : target);
|
|
252
|
+
preCheckProjectId(projectId, state, target);
|
|
253
|
+
return { state, target, projectId };
|
|
254
|
+
}
|
|
255
|
+
|
|
208
256
|
/**
|
|
209
257
|
* 校验文件夹必需文件
|
|
210
258
|
*/
|
|
@@ -446,7 +494,26 @@ async function uploadZip(zipPath, state, options) {
|
|
|
446
494
|
async function main() {
|
|
447
495
|
const args = parseArgs(process.argv.slice(2));
|
|
448
496
|
|
|
449
|
-
if (
|
|
497
|
+
if (args['check-config']) {
|
|
498
|
+
try {
|
|
499
|
+
const specRoot = resolveSpecRoot(args.project);
|
|
500
|
+
const state = readKbState(specRoot);
|
|
501
|
+
if (!state.apiKey) throw new Error('尚未配置知识库 API Key');
|
|
502
|
+
const target = resolveUploadTarget(state);
|
|
503
|
+
const identityPath = path.join(specRoot, 'skywalk-sdd/project-identity.json');
|
|
504
|
+
const identity = fs.existsSync(identityPath) ? readJson(identityPath) : null;
|
|
505
|
+
if (fs.existsSync(identityPath) && (!identity || typeof identity.project_id !== 'string' || !identity.project_id.trim())) throw new Error('项目身份文件缺少 project_id,已保留原文件,请先修复');
|
|
506
|
+
const provisional = identity && !identity.kb_space_id && typeof identity._warning === 'string'
|
|
507
|
+
&& identity._warning.startsWith('UNCONFIGURED') && /^urn:kld:sdd:project:/.test(identity.project_id || '');
|
|
508
|
+
if (identity?.project_id && !provisional) preCheckProjectId(identity.project_id, state, target);
|
|
509
|
+
else if (!target.spaceKey) throw new Error('上传目标缺少项目标识 spaceKey;请在助手重新登录获取知识库列表,或运行 /opsx-kb-config 补齐');
|
|
510
|
+
console.log(JSON.stringify({ configFile: resolveKbStatePath(specRoot), api: state.api,
|
|
511
|
+
uploadTarget: { spaceId: target.spaceId, kbId: target.kbId, spaceKey: target.spaceKey, spaceName: target.spaceName, kbName: target.kbName }, projectId: identity?.project_id || null,
|
|
512
|
+
identityWillBeCreated: !identity && Boolean(target.spaceKey), identityWillBeBound: Boolean(provisional) }, null, 2));
|
|
513
|
+
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (!args.folder && !args.package && !args.change) {
|
|
450
517
|
console.error('用法:');
|
|
451
518
|
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]');
|
|
452
519
|
console.error(' node skywalk-sdd/kb-upload.cjs --package=<zip-path>');
|
|
@@ -483,6 +550,7 @@ async function main() {
|
|
|
483
550
|
const result = await uploadZip(zipPath, state, {
|
|
484
551
|
spaceId: args['space-id'],
|
|
485
552
|
kbId: args['kb-id'],
|
|
553
|
+
overrideUploadTarget: Boolean(args['override-upload-target']),
|
|
486
554
|
});
|
|
487
555
|
|
|
488
556
|
console.log('\n── 上传结果 ──');
|
|
@@ -550,130 +618,11 @@ async function main() {
|
|
|
550
618
|
return;
|
|
551
619
|
}
|
|
552
620
|
|
|
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));
|
|
621
|
+
// Direct JSON transport reads the existing documents; ZIP remains for --package compatibility.
|
|
628
622
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
}
|
|
671
|
-
|
|
672
|
-
} finally {
|
|
673
|
-
// 清理临时目录
|
|
674
|
-
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
675
|
-
console.log('\n✓ 临时文件已清理');
|
|
676
|
-
}
|
|
623
|
+
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']) });
|
|
624
|
+
console.log(JSON.stringify(result, null, 2));
|
|
625
|
+
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
626
|
|
|
678
627
|
} catch (err) {
|
|
679
628
|
console.error(`\n❌ ${err.message}`);
|
|
@@ -681,5 +630,6 @@ async function main() {
|
|
|
681
630
|
}
|
|
682
631
|
}
|
|
683
632
|
|
|
633
|
+
module.exports = { resolveUploadTarget, prepareUploadConfig, readKbState, readArchiveCache, updateKbStateCache, assertIngestionResult, uploadZip, buildPackageFromFolder, validateRequiredFiles };
|
|
634
|
+
|
|
684
635
|
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
|
}
|