@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/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
- if (!options.caller.global) {
9093
- clauses.push("(d.scope = 'personal' AND d.owner_id = ?)");
9094
- params.push(options.caller.callerId);
9095
- if (options.caller.departmentCodes.length > 0) {
9096
- clauses.push(`(d.scope = 'department' AND d.department_code IN (${options.caller.departmentCodes.map(() => "?").join(",")}))`);
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 relaxed = tokens.length > 1 && rows.length < 2;
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
- if (isAlreadyMarkdown(relPath)) {
13393
- extractionQueue.enqueue(relPath, "");
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(relPath)) return writeJson(res, 200, {
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, relPath);
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, relPath)) return writeJson(res, 403, {
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(relPath);
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(relPath, settings.mineruBaseUrl);
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(relPath);
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"
@@ -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: "Progress";
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;
@@ -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.29",
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
- "万相AI使用教程.html",
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;