@sparkelf/dsh-knowledge-base 0.1.29 → 0.1.31
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/lib/client.js +1747 -2898
- package/lib/client.js.map +1 -1
- package/lib/index.js +73 -21
- package/lib/types/api.d.ts +1 -0
- package/lib/types/client/KnowledgeHome.d.ts +2 -0
- package/lib/types/client/KnowledgeProgressPopover.d.ts +9 -1
- package/lib/types/client/locales.d.ts +32 -3
- package/lib/types/client/progressRules.d.ts +7 -0
- package/lib/types/client/taskQueueStore.d.ts +60 -0
- package/lib/types/ftsIndex.d.ts +12 -0
- package/package.json +2 -2
- package/lib/types/client/KnowledgePermissionOverview.d.ts +0 -143
- package/lib/types/client/KnowledgePermissions.d.ts +0 -47
- package/lib/types/client/permissionRows.d.ts +0 -265
- package/lib/types/client/roleSummary.d.ts +0 -54
package/lib/index.js
CHANGED
|
@@ -9053,6 +9053,31 @@ async function syncIndexUnlocked(options) {
|
|
|
9053
9053
|
* few unreadable descendants cannot starve the result; the returned list is still
|
|
9054
9054
|
* capped at `topK`.
|
|
9055
9055
|
*/
|
|
9056
|
+
/**
|
|
9057
|
+
* 把一段连续中文切成长度 3 的滑动窗口(去重,最多 `limit` 个,默认 12)。
|
|
9058
|
+
*
|
|
9059
|
+
* trigram FTS 只能匹配长度 ≥3 的片段:整句中文当 phrase 查通常 0 命中,
|
|
9060
|
+
* 而窗口化之后「加油站」这样的 trigram 就能命中索引,再按 chunk 合并回来。
|
|
9061
|
+
*
|
|
9062
|
+
* 成本上限(有意为之,别去掉):只有在「整句 phrase 查询返回 0 行」时才会走到这里,
|
|
9063
|
+
* 且最多展开 `limit` 个窗口 → 最多多跑 `limit` 次 FTS 查询;每个窗口仍受原来的
|
|
9064
|
+
* `candidateWindow`(topK*4)限制,所以单次检索的 SQL 次数与返回量都是常数级的。
|
|
9065
|
+
* 12 个窗口足够覆盖一句 14 字左右的中文提问里最有信息量的前缀片段;再长也不会增加开销。
|
|
9066
|
+
*/
|
|
9067
|
+
function trigramWindows(text, limit = 12) {
|
|
9068
|
+
const chars = [...text].filter((char) => /[\p{Script=Han}A-Za-z0-9]/u.test(char));
|
|
9069
|
+
if (chars.length < 3) return [];
|
|
9070
|
+
const windows = [];
|
|
9071
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9072
|
+
for (let i = 0; i + 3 <= chars.length; i += 1) {
|
|
9073
|
+
const window = chars.slice(i, i + 3).join("");
|
|
9074
|
+
if (seen.has(window)) continue;
|
|
9075
|
+
seen.add(window);
|
|
9076
|
+
windows.push(window);
|
|
9077
|
+
if (windows.length >= limit) break;
|
|
9078
|
+
}
|
|
9079
|
+
return windows;
|
|
9080
|
+
}
|
|
9056
9081
|
async function searchIndex(options) {
|
|
9057
9082
|
return withIndexLock(() => withBusyRetry(() => searchIndexUnlocked(options)));
|
|
9058
9083
|
}
|
|
@@ -9089,13 +9114,11 @@ async function searchIndexUnlocked(options) {
|
|
|
9089
9114
|
ensureChunkColumns(db);
|
|
9090
9115
|
const clauses = ["d.scope = 'global'"];
|
|
9091
9116
|
const params = [];
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
params.push(...options.caller.departmentCodes);
|
|
9098
|
-
}
|
|
9117
|
+
clauses.push("(d.scope = 'personal' AND d.owner_id = ?)");
|
|
9118
|
+
params.push(options.caller.callerId);
|
|
9119
|
+
if (options.caller.departmentCodes.length > 0) {
|
|
9120
|
+
clauses.push(`(d.scope = 'department' AND d.department_code IN (${options.caller.departmentCodes.map(() => "?").join(",")}))`);
|
|
9121
|
+
params.push(...options.caller.departmentCodes);
|
|
9099
9122
|
}
|
|
9100
9123
|
const narrowed = options.scope === "personal" || options.scope === "department" || options.scope === "global" ? options.scope : void 0;
|
|
9101
9124
|
const scopeClause = narrowed === void 0 ? "" : " AND d.scope = ?";
|
|
@@ -9133,7 +9156,8 @@ async function searchIndexUnlocked(options) {
|
|
|
9133
9156
|
};
|
|
9134
9157
|
const rows = runTerm(query);
|
|
9135
9158
|
const tokens = query.split(/\s+/u).map((token) => token.trim()).filter((token) => token !== "");
|
|
9136
|
-
const
|
|
9159
|
+
const cjkWindows = tokens.length === 1 && rows.length === 0 ? trigramWindows(query) : [];
|
|
9160
|
+
const relaxed = tokens.length > 1 && rows.length < 2 || cjkWindows.length > 0;
|
|
9137
9161
|
const merged = /* @__PURE__ */ new Map();
|
|
9138
9162
|
const addRows = (candidates) => {
|
|
9139
9163
|
for (const row of candidates) {
|
|
@@ -9142,7 +9166,7 @@ async function searchIndexUnlocked(options) {
|
|
|
9142
9166
|
if (existing === void 0 || row.score < existing.score) merged.set(key, row);
|
|
9143
9167
|
}
|
|
9144
9168
|
};
|
|
9145
|
-
const queries = relaxed ? tokens.slice(0, 8) : [query];
|
|
9169
|
+
const queries = relaxed ? cjkWindows.length > 0 ? cjkWindows : tokens.slice(0, 8) : [query];
|
|
9146
9170
|
if (relaxed) for (const token of queries) addRows(runTerm(token));
|
|
9147
9171
|
else addRows(rows);
|
|
9148
9172
|
const ordered = [...merged.values()].sort((a, b) => {
|
|
@@ -11774,6 +11798,7 @@ function permissionForKbRoute(method) {
|
|
|
11774
11798
|
if (method === "create" || method === "edit" || method === "move" || method === "upload" || method === "rename") return "kb:file:write";
|
|
11775
11799
|
if (method === "trash" || method === "trash.remove" || method === "trash.restore") return "kb:file:delete";
|
|
11776
11800
|
if (method === "index" || method === "extract.retry") return "kb:file:extract";
|
|
11801
|
+
if (method === "index.status") return "kb:file:read";
|
|
11777
11802
|
if (method === "audit.list") return "kb:audit:read";
|
|
11778
11803
|
if (method === "grants" || method === "grant" || method === "revoke") return "kb:grant:manage";
|
|
11779
11804
|
if (method === "pending.list" || method === "pending.approve" || method === "pending.reject") return "kb:review:decide";
|
|
@@ -13375,6 +13400,32 @@ function apply(ctx, deploymentConfig) {
|
|
|
13375
13400
|
});
|
|
13376
13401
|
}
|
|
13377
13402
|
case "extract.status": return writeJson(res, 200, extractionQueue.snapshot());
|
|
13403
|
+
case "index.status": {
|
|
13404
|
+
const body = await readJsonBody(req);
|
|
13405
|
+
const relPath = typeof body.path === "string" ? body.path.trim() : "";
|
|
13406
|
+
const docId = Number(body.docId);
|
|
13407
|
+
const db = new DatabaseSync(indexPathFor(config.indexDir));
|
|
13408
|
+
try {
|
|
13409
|
+
let row;
|
|
13410
|
+
if (Number.isInteger(docId) && docId > 0) row = db.prepare("select id, path, scope, mtime from kb_docs where id = ?").get(docId);
|
|
13411
|
+
else if (relPath !== "") {
|
|
13412
|
+
const name = relPath.slice(relPath.lastIndexOf("/") + 1);
|
|
13413
|
+
row = db.prepare("select id, path, scope, mtime from kb_docs where path = ?").get(relPath) ?? db.prepare("select id, path, scope, mtime from kb_docs where path = ? order by mtime desc limit 1").get(name) ?? db.prepare("select id, path, scope, mtime from kb_docs where path like ? order by mtime desc limit 1").get("%/" + relPath) ?? db.prepare("select id, path, scope, mtime from kb_docs where path like ? order by mtime desc limit 1").get("%" + relPath) ?? db.prepare("select id, path, scope, mtime from kb_docs where path like ? order by mtime desc limit 1").get("%/" + name);
|
|
13414
|
+
}
|
|
13415
|
+
if (row === void 0) return writeJson(res, 200, { found: false });
|
|
13416
|
+
const count = db.prepare("select count(*) as n from kb_chunks where doc_id = ?").get(row.id);
|
|
13417
|
+
return writeJson(res, 200, {
|
|
13418
|
+
found: true,
|
|
13419
|
+
docId: row.id,
|
|
13420
|
+
path: row.path,
|
|
13421
|
+
scope: row.scope,
|
|
13422
|
+
chunks: Number(count?.n ?? 0),
|
|
13423
|
+
indexedAt: row.mtime
|
|
13424
|
+
});
|
|
13425
|
+
} finally {
|
|
13426
|
+
db.close();
|
|
13427
|
+
}
|
|
13428
|
+
}
|
|
13378
13429
|
case "extract.retry": {
|
|
13379
13430
|
const body = await readJsonBody(req);
|
|
13380
13431
|
const relPath = String(body.relPath ?? "");
|
|
@@ -13389,32 +13440,33 @@ function apply(ctx, deploymentConfig) {
|
|
|
13389
13440
|
error: "path escapes the knowledge root",
|
|
13390
13441
|
refused: [relPath]
|
|
13391
13442
|
});
|
|
13392
|
-
|
|
13393
|
-
|
|
13443
|
+
const actualRelPath = await actualRelPathForView(host, config, relPath).catch(() => relPath);
|
|
13444
|
+
if (isAlreadyMarkdown(actualRelPath)) {
|
|
13445
|
+
extractionQueue.enqueue(actualRelPath, "");
|
|
13394
13446
|
return writeJson(res, 200, {
|
|
13395
13447
|
outcome: "already-markdown",
|
|
13396
|
-
relPath,
|
|
13448
|
+
relPath: actualRelPath,
|
|
13397
13449
|
message: "this document is already Markdown"
|
|
13398
13450
|
});
|
|
13399
13451
|
}
|
|
13400
|
-
if (!isExtractable(
|
|
13452
|
+
if (!isExtractable(actualRelPath)) return writeJson(res, 200, {
|
|
13401
13453
|
outcome: "unsupported",
|
|
13402
|
-
relPath,
|
|
13454
|
+
relPath: actualRelPath,
|
|
13403
13455
|
message: "this file type has no extractor"
|
|
13404
13456
|
});
|
|
13405
13457
|
const localRoot = localConfig(config);
|
|
13406
|
-
const target = join(localRoot.actualRoot,
|
|
13458
|
+
const target = join(localRoot.actualRoot, actualRelPath);
|
|
13407
13459
|
const source = await stat(target).catch(() => void 0);
|
|
13408
13460
|
if (source === void 0 || !source.isFile()) return writeJson(res, 404, { error: "no such file" });
|
|
13409
|
-
if (!await canWritePath(host, config,
|
|
13461
|
+
if (!await canWritePath(host, config, actualRelPath)) return writeJson(res, 403, {
|
|
13410
13462
|
error: "write access is required to extract here",
|
|
13411
13463
|
refused: [relPath]
|
|
13412
13464
|
});
|
|
13413
|
-
const sibling = markdownSiblingOf(
|
|
13465
|
+
const sibling = markdownSiblingOf(actualRelPath);
|
|
13414
13466
|
const existing = await stat(join(localRoot.actualRoot, sibling)).catch(() => void 0);
|
|
13415
13467
|
if (existing !== void 0 && existing.isFile()) return writeJson(res, 200, {
|
|
13416
13468
|
outcome: "exists",
|
|
13417
|
-
relPath,
|
|
13469
|
+
relPath: actualRelPath,
|
|
13418
13470
|
markdown: sibling
|
|
13419
13471
|
});
|
|
13420
13472
|
const settings = await readSettings(viewPathFor({
|
|
@@ -13422,13 +13474,13 @@ function apply(ctx, deploymentConfig) {
|
|
|
13422
13474
|
viewRoot: localRoot.viewRoot,
|
|
13423
13475
|
userId: (await fetchCaller(host, config).catch(() => void 0))?.userId ?? ""
|
|
13424
13476
|
}));
|
|
13425
|
-
const accepted = extractionQueue.enqueue(
|
|
13477
|
+
const accepted = extractionQueue.enqueue(actualRelPath, settings.mineruBaseUrl);
|
|
13426
13478
|
if (body.wait === false) return writeJson(res, 202, {
|
|
13427
13479
|
outcome: "queued",
|
|
13428
|
-
relPath,
|
|
13480
|
+
relPath: actualRelPath,
|
|
13429
13481
|
extraction: accepted
|
|
13430
13482
|
});
|
|
13431
|
-
const settled = await extractionQueue.settled(
|
|
13483
|
+
const settled = await extractionQueue.settled(actualRelPath);
|
|
13432
13484
|
const outcome = settled === void 0 ? {
|
|
13433
13485
|
state: "failed",
|
|
13434
13486
|
reason: "the extraction task was not recorded"
|
package/lib/types/api.d.ts
CHANGED
|
@@ -138,6 +138,7 @@ export declare const KB_API: {
|
|
|
138
138
|
readonly extractStatus: "extract.status";
|
|
139
139
|
/** Re-run extraction for one document that could not finish. */
|
|
140
140
|
readonly extractRetry: "extract.retry";
|
|
141
|
+
readonly indexStatus: "index.status";
|
|
141
142
|
/** Read the caller's pinned paths. */
|
|
142
143
|
readonly favoritesGet: "favorites.get";
|
|
143
144
|
readonly favoritesSet: "favorites.set";
|
|
@@ -25,6 +25,8 @@ interface HomeRow {
|
|
|
25
25
|
location: string;
|
|
26
26
|
/** Last-modified instant in Unix ms, which is the tree's only timestamp. */
|
|
27
27
|
mtimeMs?: number;
|
|
28
|
+
/** Host's answer: a `.md` sibling exists beside this non-text document. */
|
|
29
|
+
hasMarkdown?: boolean;
|
|
28
30
|
}
|
|
29
31
|
/** Props for the Home surface. */
|
|
30
32
|
export interface KnowledgeHomeProps {
|
|
@@ -60,10 +60,18 @@ export interface KnowledgeProgressPopoverProps {
|
|
|
60
60
|
* exists to remove. The page owns the store; this component only draws it.
|
|
61
61
|
*/
|
|
62
62
|
failures?: readonly FailedExtractionView[];
|
|
63
|
+
/**
|
|
64
|
+
* Keep the persistence/polling work but render no bell or modal.
|
|
65
|
+
*
|
|
66
|
+
* The queue now lives in the Task Center's notifications tab, which reads the same
|
|
67
|
+
* localStorage key; the Knowledge page only needs this component mounted to keep
|
|
68
|
+
* writing observations.
|
|
69
|
+
*/
|
|
70
|
+
headless?: boolean;
|
|
63
71
|
}
|
|
64
72
|
/**
|
|
65
73
|
* The notification button and its progress panel.
|
|
66
74
|
* @param props - the upload batch, whether to watch extraction, and the copy resolver.
|
|
67
75
|
* @returns The button, plus the panel while it is open.
|
|
68
76
|
*/
|
|
69
|
-
export declare function KnowledgeProgressPopover(props: KnowledgeProgressPopoverProps): import("react").JSX.Element;
|
|
77
|
+
export declare function KnowledgeProgressPopover(props: KnowledgeProgressPopoverProps): import("react").JSX.Element | null;
|
|
@@ -55,7 +55,6 @@ export declare const en: {
|
|
|
55
55
|
readonly renameFailed: "Could not rename: the file may already exist or you may not have write access.";
|
|
56
56
|
readonly downloadFailed: "Could not download this file.";
|
|
57
57
|
readonly 'menu.home': "Home";
|
|
58
|
-
readonly 'menu.permissions': "Permissions";
|
|
59
58
|
readonly 'menu.indexing': "Directories & counts";
|
|
60
59
|
readonly 'menu.trash': "Trash";
|
|
61
60
|
readonly brand: "Wanxiang Knowledge";
|
|
@@ -115,6 +114,24 @@ export declare const en: {
|
|
|
115
114
|
readonly homeColOwnerUnset: "Not recorded";
|
|
116
115
|
readonly homeColCreated: "Created";
|
|
117
116
|
readonly homeColOpened: "Last opened";
|
|
117
|
+
readonly homeColStatus: "Status";
|
|
118
|
+
readonly homeStatusExtracted: "Extracted";
|
|
119
|
+
readonly homeStatusNotExtracted: "Not extracted";
|
|
120
|
+
readonly homeStatusIndexed: "Indexed";
|
|
121
|
+
readonly homeStatusNotIndexed: "Not indexed";
|
|
122
|
+
readonly statusFileName: "File";
|
|
123
|
+
readonly statusPath: "Path";
|
|
124
|
+
readonly statusType: "Type";
|
|
125
|
+
readonly statusTypeText: "Text";
|
|
126
|
+
readonly statusTypeNonText: "Document";
|
|
127
|
+
readonly statusExtract: "Extraction";
|
|
128
|
+
readonly statusExtractTime: "Extracted at";
|
|
129
|
+
readonly statusMdSize: ".md size";
|
|
130
|
+
readonly statusIndex: "Index";
|
|
131
|
+
readonly statusIndexTime: "Indexed at";
|
|
132
|
+
readonly statusChunks: "Chunks / docId";
|
|
133
|
+
readonly statusFailure: "Failure reason";
|
|
134
|
+
readonly statusClose: "Close";
|
|
118
135
|
readonly homeEmpty: "Nothing here yet.";
|
|
119
136
|
readonly trashEmpty: "Trash is empty";
|
|
120
137
|
readonly trashPending: "Deleting a document is not enabled yet. The knowledge tree is shared, and how long a deletion is kept is still being decided.";
|
|
@@ -325,6 +342,10 @@ export declare const en: {
|
|
|
325
342
|
readonly auditActionUpload: "Upload";
|
|
326
343
|
readonly auditActionMove: "Move";
|
|
327
344
|
readonly auditActionEdit: "Edit";
|
|
345
|
+
readonly auditActionDelete: "Delete";
|
|
346
|
+
readonly auditActionExternalChange: "External change";
|
|
347
|
+
readonly auditActorExternalChange: "External process";
|
|
348
|
+
readonly auditActorSystem: "System";
|
|
328
349
|
readonly auditOutcomeApplied: "Applied";
|
|
329
350
|
readonly auditOutcomePending: "Pending review";
|
|
330
351
|
readonly storageProtocol: "Protocol";
|
|
@@ -373,6 +394,7 @@ export declare const en: {
|
|
|
373
394
|
readonly progressBadgeMore: "99+";
|
|
374
395
|
readonly progressOpen: "Upload and extraction progress";
|
|
375
396
|
readonly progressClose: "Close";
|
|
397
|
+
readonly progressClear: "Clear history";
|
|
376
398
|
readonly dropRefusedNoCaller: "Cannot place this upload";
|
|
377
399
|
readonly dropRefusedNoCallerReason: "The host has not reported your account yet, so the personal folder cannot be resolved. Reload and try again.";
|
|
378
400
|
readonly progressResize: "Drag to resize";
|
|
@@ -466,7 +488,14 @@ export declare const en: {
|
|
|
466
488
|
readonly refPickerBatchTitle: "Reference {n} items in a chat";
|
|
467
489
|
readonly refPickerFailed: "Could not insert the reference into that chat.";
|
|
468
490
|
readonly dropHomeHint: "Drop to upload into your personal folder";
|
|
469
|
-
readonly progressTitle: "
|
|
491
|
+
readonly progressTitle: "Task queue";
|
|
492
|
+
readonly progressQueueTitle: "Tasks";
|
|
493
|
+
readonly progressUploadDone: "Uploaded";
|
|
494
|
+
readonly progressExtractDone: "Extraction done";
|
|
495
|
+
readonly progressStepUploading: "Uploading";
|
|
496
|
+
readonly progressStepUploaded: "Uploaded";
|
|
497
|
+
readonly progressStepExtracting: "Extracting";
|
|
498
|
+
readonly progressStepExtracted: "Extracted";
|
|
470
499
|
readonly progressRoot: "Root";
|
|
471
500
|
readonly overviewByRole: "By role";
|
|
472
501
|
readonly sourceDirect: "Direct";
|
|
@@ -496,7 +525,7 @@ export declare const en: {
|
|
|
496
525
|
readonly progressExtractGroup: "Extractions";
|
|
497
526
|
readonly progressUploading: "Uploading {done}/{total}";
|
|
498
527
|
readonly progressPending: "Queued";
|
|
499
|
-
readonly progressExtracting: "Extracting";
|
|
528
|
+
readonly progressExtracting: "Extracting…";
|
|
500
529
|
readonly progressStepParse: "Parsing document";
|
|
501
530
|
readonly progressStepChunk: "Chunking text";
|
|
502
531
|
readonly progressStepIndex: "Writing FTS5 index";
|
|
@@ -87,6 +87,10 @@ export interface UploadTaskView {
|
|
|
87
87
|
state: 'waiting' | 'uploading' | 'done' | 'failed';
|
|
88
88
|
/** Why it failed, in the Host's own words. */
|
|
89
89
|
reason?: string;
|
|
90
|
+
/**
|
|
91
|
+
* When this row was enqueued, ISO-8601. Used to order the unified task queue.
|
|
92
|
+
*/
|
|
93
|
+
at?: string;
|
|
90
94
|
/**
|
|
91
95
|
* Bytes sent, as 0-100.
|
|
92
96
|
*
|
|
@@ -112,9 +116,12 @@ export interface UploadTaskView {
|
|
|
112
116
|
*/
|
|
113
117
|
export declare function pendingCount(uploads: readonly {
|
|
114
118
|
state: string;
|
|
119
|
+
at?: string;
|
|
115
120
|
}[], extractions: readonly {
|
|
116
121
|
state: string;
|
|
117
122
|
}[]): number;
|
|
123
|
+
/** 上传完成后继续轮询提取任务的窗口。 */
|
|
124
|
+
export declare const UPLOAD_EXTRACTION_GRACE_MS = 30000;
|
|
118
125
|
/**
|
|
119
126
|
* How long an optimistic extraction entry may wait for the Host to confirm it.
|
|
120
127
|
*
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local persistence for the task queue.
|
|
3
|
+
*
|
|
4
|
+
* Only metadata is stored: a reader who comes back after a reload should still see what
|
|
5
|
+
* they uploaded or extracted, not the document bytes. Records older than the retention
|
|
6
|
+
* window are dropped on load, and the list is capped so a long-lived profile cannot grow
|
|
7
|
+
* without bound.
|
|
8
|
+
*
|
|
9
|
+
* @module @sparkelf/dsh-knowledge-base/client
|
|
10
|
+
*/
|
|
11
|
+
/** One task as it is remembered across reloads. */
|
|
12
|
+
export interface PersistedTask {
|
|
13
|
+
/** Stable queue id (`upload:<path>` / `extract:<path>`). */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Display name of the source document. */
|
|
16
|
+
fileName: string;
|
|
17
|
+
/** Which operation the task represents. */
|
|
18
|
+
kind: 'upload' | 'extract';
|
|
19
|
+
/** Last observed lifecycle state. */
|
|
20
|
+
stage: 'uploading' | 'uploaded' | 'extracting' | 'done' | 'failed';
|
|
21
|
+
/** Upload percentage, 0-100, when known. */
|
|
22
|
+
progress?: number;
|
|
23
|
+
/** When the task was first seen, epoch milliseconds. */
|
|
24
|
+
startedAt: number;
|
|
25
|
+
/** When it reached a terminal state, epoch milliseconds. */
|
|
26
|
+
finishedAt?: number;
|
|
27
|
+
/** Failure reason, when the task failed. */
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
/** The localStorage key this plugin owns. */
|
|
31
|
+
export declare const TASK_QUEUE_STORAGE_KEY = "dataops-kb-task-queue-v1";
|
|
32
|
+
/** How long a record is kept. */
|
|
33
|
+
export declare const TASK_QUEUE_RETENTION_MS: number;
|
|
34
|
+
/** Upper bound on records, newest first. */
|
|
35
|
+
export declare const TASK_QUEUE_MAX_RECORDS = 200;
|
|
36
|
+
/**
|
|
37
|
+
* Read the retained queue.
|
|
38
|
+
* @param now - current epoch milliseconds, injectable for tests.
|
|
39
|
+
* @returns Newest-first records inside the retention window.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadTaskQueue(now?: number): PersistedTask[];
|
|
42
|
+
/**
|
|
43
|
+
* Persist the queue, silently degrading when storage is unavailable.
|
|
44
|
+
* @param records - the records to write, already ordered.
|
|
45
|
+
*/
|
|
46
|
+
export declare function saveTaskQueue(records: readonly PersistedTask[]): void;
|
|
47
|
+
/**
|
|
48
|
+
* Merge freshly observed rows into the stored history.
|
|
49
|
+
*
|
|
50
|
+
* A new observation wins for the mutable fields (stage/progress/error), while the first
|
|
51
|
+
* `startedAt` and the first terminal `finishedAt` are preserved so a task does not appear
|
|
52
|
+
* to restart on every poll.
|
|
53
|
+
* @param current - stored records.
|
|
54
|
+
* @param incoming - rows observed this render.
|
|
55
|
+
* @param now - current epoch milliseconds.
|
|
56
|
+
* @returns Newest-first merged records, capped.
|
|
57
|
+
*/
|
|
58
|
+
export declare function mergeTaskRecords(current: readonly PersistedTask[], incoming: readonly PersistedTask[], now?: number): PersistedTask[];
|
|
59
|
+
/** Remove every stored record. */
|
|
60
|
+
export declare function clearTaskQueue(): void;
|
package/lib/types/ftsIndex.d.ts
CHANGED
|
@@ -287,6 +287,18 @@ export declare function indexStats(indexPath: string): Promise<{
|
|
|
287
287
|
* few unreadable descendants cannot starve the result; the returned list is still
|
|
288
288
|
* capped at `topK`.
|
|
289
289
|
*/
|
|
290
|
+
/**
|
|
291
|
+
* 把一段连续中文切成长度 3 的滑动窗口(去重,最多 `limit` 个,默认 12)。
|
|
292
|
+
*
|
|
293
|
+
* trigram FTS 只能匹配长度 ≥3 的片段:整句中文当 phrase 查通常 0 命中,
|
|
294
|
+
* 而窗口化之后「加油站」这样的 trigram 就能命中索引,再按 chunk 合并回来。
|
|
295
|
+
*
|
|
296
|
+
* 成本上限(有意为之,别去掉):只有在「整句 phrase 查询返回 0 行」时才会走到这里,
|
|
297
|
+
* 且最多展开 `limit` 个窗口 → 最多多跑 `limit` 次 FTS 查询;每个窗口仍受原来的
|
|
298
|
+
* `candidateWindow`(topK*4)限制,所以单次检索的 SQL 次数与返回量都是常数级的。
|
|
299
|
+
* 12 个窗口足够覆盖一句 14 字左右的中文提问里最有信息量的前缀片段;再长也不会增加开销。
|
|
300
|
+
*/
|
|
301
|
+
export declare function trigramWindows(text: string, limit?: number): string[];
|
|
290
302
|
export declare function searchIndex(options: {
|
|
291
303
|
indexPath: string;
|
|
292
304
|
query: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-knowledge-base",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "DataOps-backed knowledge base for DeepSeek Harness: a left-sidebar destination, Settings configuration, and index tooling over a mounted NAS view.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"README.md",
|
|
44
44
|
"LICENSE",
|
|
45
45
|
"SKILL.md",
|
|
46
|
-
"
|
|
46
|
+
"\u4e07\u76f8AI\u4f7f\u7528\u6559\u7a0b.html",
|
|
47
47
|
"i18n/*.json",
|
|
48
48
|
"skills/**/SKILL.md",
|
|
49
49
|
"skills/**/i18n/*.json"
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The permissions surface: one grant table, read two ways.
|
|
3
|
-
*
|
|
4
|
-
* A grant says "this principal may do this action to this path". That is the right
|
|
5
|
-
* shape to STORE and the wrong shape to AUDIT: an administrator asking "what does
|
|
6
|
-
* 张三 have" and "who can see 制度.md" is asking two questions that one
|
|
7
|
-
* row-per-grant layout cannot answer without being read line by line.
|
|
8
|
-
*
|
|
9
|
-
* So the page has one body and two readings of it. The tabs switch which question
|
|
10
|
-
* is being asked; both render the same loaded grants, so the two can never
|
|
11
|
-
* disagree. No second endpoint exists because there is no second truth.
|
|
12
|
-
*
|
|
13
|
-
* The tables are the page's own body rather than a summary above one. An earlier
|
|
14
|
-
* version stacked both readings as bulleted lists and *also* drew a third,
|
|
15
|
-
* row-per-principal table underneath: three renderings of one payload, so the
|
|
16
|
-
* reader had to work out which of the three was authoritative before reading any
|
|
17
|
-
* of them. Two tabs over one table removes that question.
|
|
18
|
-
*
|
|
19
|
-
* @module @sparkelf/dsh-knowledge-base/client/KnowledgePermissionOverview
|
|
20
|
-
*/
|
|
21
|
-
import type { GrantView, KnowledgeGrantSubjectType } from '../api.ts';
|
|
22
|
-
/** Which reading of the grant table the tabs are showing. */
|
|
23
|
-
export type OverviewTab = 'person' | 'role' | 'resource';
|
|
24
|
-
/** Copy for the overview, supplied by the caller's locale record. */
|
|
25
|
-
export interface OverviewCopy {
|
|
26
|
-
tabByPerson: string;
|
|
27
|
-
tabByRole: string;
|
|
28
|
-
tabByResource: string;
|
|
29
|
-
empty: string;
|
|
30
|
-
/** Empty state per view, so "nobody is directly granted" is not shown for a role. */
|
|
31
|
-
emptyByPerson: string;
|
|
32
|
-
emptyByRole: string;
|
|
33
|
-
emptyByResource: string;
|
|
34
|
-
/** Where a principal's access comes from. */
|
|
35
|
-
sourceDirect: string;
|
|
36
|
-
/** Format "inherited via role X": `{role}` is replaced by the role code. */
|
|
37
|
-
sourceViaRole: (role: string) => string;
|
|
38
|
-
/** Shown when the user directory could not be read, so inheritance is unknowable. */
|
|
39
|
-
sourceUnavailable: string;
|
|
40
|
-
/** Word for a permission set that could not be fully determined. */
|
|
41
|
-
actionsUnknown: string;
|
|
42
|
-
actionsUnknownHint: string;
|
|
43
|
-
/** Column headers unique to the three views. */
|
|
44
|
-
colAccount: string;
|
|
45
|
-
colSource: string;
|
|
46
|
-
/** "{count} member(s)". */
|
|
47
|
-
roleMemberCount: (count: number) => string;
|
|
48
|
-
/** Pager copy, shared with the audit table's wording. */
|
|
49
|
-
pagerPrev: string;
|
|
50
|
-
pagerNext: string;
|
|
51
|
-
pagerStatus: (page: number, pageCount: number, total: number) => string;
|
|
52
|
-
pageSizeLabel: string;
|
|
53
|
-
pageSizeOption: (size: number) => string;
|
|
54
|
-
pathsHeld: (count: number) => string;
|
|
55
|
-
rolesHeld: string;
|
|
56
|
-
actionsHeld: string;
|
|
57
|
-
noActions: string;
|
|
58
|
-
/** Accessible name for the tab strip, for readers who arrive at it by keyboard. */
|
|
59
|
-
tabsLabel: string;
|
|
60
|
-
/** Stat-card labels: the four numbers the page leads with. */
|
|
61
|
-
statPrincipals: string;
|
|
62
|
-
statResources: string;
|
|
63
|
-
statGrants: string;
|
|
64
|
-
statRoles: string;
|
|
65
|
-
/** Shared column headers, person reading. */
|
|
66
|
-
colPrincipal: string;
|
|
67
|
-
colKind: string;
|
|
68
|
-
colPermission: string;
|
|
69
|
-
colResources: string;
|
|
70
|
-
colExpiry: string;
|
|
71
|
-
colActions: string;
|
|
72
|
-
/** Shared column headers, resource reading. */
|
|
73
|
-
colResource: string;
|
|
74
|
-
colHolders: string;
|
|
75
|
-
colKindOfResource: string;
|
|
76
|
-
revoke: string;
|
|
77
|
-
revokeBusy: string;
|
|
78
|
-
expiryNever: string;
|
|
79
|
-
kindFile: string;
|
|
80
|
-
kindDirectory: string;
|
|
81
|
-
/** Formats "N grants · M paths" under a person's name. */
|
|
82
|
-
grantsUnit: (count: number) => string;
|
|
83
|
-
resourcesUnit: (count: number) => string;
|
|
84
|
-
/**
|
|
85
|
-
* Resolves a principal kind and an action to its localized word.
|
|
86
|
-
*
|
|
87
|
-
* Supplied as callbacks rather than as a keyed map so this component never
|
|
88
|
-
* reaches into the locale record itself: a caller with its own wording (a test,
|
|
89
|
-
* another surface) can pass its own, and the type checker still enforces that
|
|
90
|
-
* every kind and action the table can print has a word.
|
|
91
|
-
*/
|
|
92
|
-
kindLabel: (kind: KnowledgeGrantSubjectType) => string;
|
|
93
|
-
actionLabel: (action: string) => string;
|
|
94
|
-
}
|
|
95
|
-
/** Props the parent panel supplies. */
|
|
96
|
-
export interface KnowledgePermissionOverviewProps {
|
|
97
|
-
/** The grant table already loaded by the permissions page. */
|
|
98
|
-
grants: readonly GrantView[];
|
|
99
|
-
/** Display names by `principalType:principalId`. */
|
|
100
|
-
labels?: Readonly<Record<string, string>>;
|
|
101
|
-
/**
|
|
102
|
-
* The DataOps account directory, when the caller could read it.
|
|
103
|
-
*
|
|
104
|
-
* Absent means the same as an unavailable read: inheritance cannot be attributed, and
|
|
105
|
-
* the page says so instead of listing accounts that look like they hold nothing.
|
|
106
|
-
*/
|
|
107
|
-
directory?: {
|
|
108
|
-
kind: 'available';
|
|
109
|
-
users: ReadonlyArray<{
|
|
110
|
-
id: string;
|
|
111
|
-
label: string;
|
|
112
|
-
username?: string;
|
|
113
|
-
roleCodes?: string[];
|
|
114
|
-
}>;
|
|
115
|
-
} | {
|
|
116
|
-
kind: 'unavailable';
|
|
117
|
-
reason: string;
|
|
118
|
-
};
|
|
119
|
-
/** Locale copy. */
|
|
120
|
-
copy: OverviewCopy;
|
|
121
|
-
/**
|
|
122
|
-
* Asks the parent to confirm revoking the grants one row covers.
|
|
123
|
-
*
|
|
124
|
-
* The confirmation lives in the parent because it is a dialog, and a dialog in
|
|
125
|
-
* the middle of a table's row renderer would make the table responsible for
|
|
126
|
-
* ownership of the overlay. This component says *what* to revoke; the parent
|
|
127
|
-
* decides whether to ask first and then performs it.
|
|
128
|
-
*/
|
|
129
|
-
onRevoke: (grants: GrantView[], label: string) => void;
|
|
130
|
-
/** Disables every revoke control while a write is in flight. */
|
|
131
|
-
busy?: boolean;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Render the permission overview as stat cards over a three-tab table.
|
|
135
|
-
*
|
|
136
|
-
* THREE views, not two, because the old pair answered different questions under one
|
|
137
|
-
* heading: "by person" listed ROLE grants, so the only rows were 管理员 and 超级管理员
|
|
138
|
-
* and a reader concluding "these are the people with access" was reading the page
|
|
139
|
-
* correctly and being told something false. A role is not a person.
|
|
140
|
-
*
|
|
141
|
-
* @param props - grants, labels, directory, role summaries, copy and the revoke hook.
|
|
142
|
-
*/
|
|
143
|
-
export declare function KnowledgePermissionOverview(props: KnowledgePermissionOverviewProps): import("react").JSX.Element;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The permissions surface of the management page.
|
|
3
|
-
*
|
|
4
|
-
* This view reads and writes grants through the DSH Host, which forwards them to
|
|
5
|
-
* the DataOps backend. It deliberately holds no policy of its own: the backend's
|
|
6
|
-
* authorization resolves the principal, checks the caller's permission keys, and
|
|
7
|
-
* enforces what a grant means. The page only presents the result, so a rule
|
|
8
|
-
* change lands in one place.
|
|
9
|
-
*
|
|
10
|
-
* The page leads with its table. A grant list is what an operator opens this
|
|
11
|
-
* surface to read, so the data is the body of the page and the two things that
|
|
12
|
-
* CHANGE it -- creating a grant, revoking one -- are raised into dialogs. A form
|
|
13
|
-
* parked above the table would push the answer below the fold to make room for
|
|
14
|
-
* the question.
|
|
15
|
-
*
|
|
16
|
-
* @module @sparkelf/dsh-knowledge-base/client
|
|
17
|
-
*/
|
|
18
|
-
import { type ReactNode } from 'react';
|
|
19
|
-
/** Props for the permissions surface. */
|
|
20
|
-
export interface KnowledgePermissionsProps {
|
|
21
|
-
/**
|
|
22
|
-
* Publishes this page's primary control to the panel header.
|
|
23
|
-
*
|
|
24
|
-
* The header is the only band at the top of the page, so a page-level action has
|
|
25
|
-
* to be handed up rather than drawn in a second title row.
|
|
26
|
-
*/
|
|
27
|
-
onAction?: (action: ReactNode) => void;
|
|
28
|
-
/** Locale-bound copy resolver. */
|
|
29
|
-
t: (key: string) => string;
|
|
30
|
-
/** Mount point the grants are scoped to. */
|
|
31
|
-
mountPoint: string;
|
|
32
|
-
/** Path the user selected, when the surface opened from a file row. */
|
|
33
|
-
initialPath?: string;
|
|
34
|
-
/**
|
|
35
|
-
* Changes when a row's context menu picks a new path.
|
|
36
|
-
*
|
|
37
|
-
* The form's path field is seeded from `initialPath` once; without a signal that
|
|
38
|
-
* the seed changed, a second right-click would leave the field on the first path.
|
|
39
|
-
*/
|
|
40
|
-
reloadToken?: number;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Render the permission editor.
|
|
44
|
-
* @param props - copy resolver and the mount point grants are scoped to.
|
|
45
|
-
* @returns The permission surface.
|
|
46
|
-
*/
|
|
47
|
-
export declare function KnowledgePermissions(props: KnowledgePermissionsProps): import("react").JSX.Element;
|