@sparkelf/dsh-knowledge-base 0.1.5 → 0.1.20

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.
Files changed (58) hide show
  1. package/README.md +129 -2
  2. package/SKILL.md +86 -18
  3. package/lib/client.js +14766 -9663
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +8082 -258
  6. package/lib/types/api.d.ts +36 -1
  7. package/lib/types/client/KnowledgeCreateDialog.d.ts +25 -0
  8. package/lib/types/client/KnowledgeFileTree.d.ts +143 -0
  9. package/lib/types/client/KnowledgeHome.d.ts +58 -0
  10. package/lib/types/client/KnowledgeListing.d.ts +7 -0
  11. package/lib/types/client/KnowledgeNewDocumentDialog.d.ts +32 -0
  12. package/lib/types/client/KnowledgeProgressPopover.d.ts +69 -0
  13. package/lib/types/client/KnowledgeSettings.d.ts +4 -3
  14. package/lib/types/client/KnowledgeTemplates.d.ts +4 -0
  15. package/lib/types/client/VectorSettingsGroup.d.ts +17 -0
  16. package/lib/types/client/WorkflowMenuIcon.d.ts +22 -0
  17. package/lib/types/client/extractionHistory.d.ts +43 -0
  18. package/lib/types/client/index.d.ts +32 -14
  19. package/lib/types/client/listingFilter.d.ts +58 -0
  20. package/lib/types/client/locales.d.ts +123 -0
  21. package/lib/types/client/openInSidebar.d.ts +2 -2
  22. package/lib/types/client/progressRules.d.ts +198 -17
  23. package/lib/types/client/sanitizeExplorerTabs.d.ts +20 -0
  24. package/lib/types/client/settingsGate.d.ts +65 -0
  25. package/lib/types/client/uploadTransport.d.ts +65 -0
  26. package/lib/types/client/useTreeSort.d.ts +72 -0
  27. package/lib/types/client/workflows/WorkflowPlaza.d.ts +146 -0
  28. package/lib/types/client/workflows/cron.d.ts +125 -0
  29. package/lib/types/client/workflows/dedupe.d.ts +70 -0
  30. package/lib/types/client/workflows/deps.d.ts +101 -0
  31. package/lib/types/client/workflows/modelChoice.d.ts +106 -0
  32. package/lib/types/client/workflows/published.d.ts +43 -0
  33. package/lib/types/client/workflows/registry.d.ts +98 -0
  34. package/lib/types/client/workflows/subscribe.d.ts +179 -0
  35. package/lib/types/embeddings.d.ts +62 -0
  36. package/lib/types/externalChange.d.ts +25 -0
  37. package/lib/types/extract.d.ts +32 -0
  38. package/lib/types/extractionQueue.d.ts +23 -2
  39. package/lib/types/ftsIndex.d.ts +497 -0
  40. package/lib/types/index.d.ts +266 -8
  41. package/lib/types/initialize.d.ts +105 -0
  42. package/lib/types/knowledgeTools.d.ts +239 -0
  43. package/lib/types/localStoragePath.d.ts +6 -0
  44. package/lib/types/localStorageSettings.d.ts +33 -0
  45. package/lib/types/managementTools.d.ts +184 -0
  46. package/lib/types/outlineGuard.d.ts +119 -0
  47. package/lib/types/outlineText.d.ts +134 -0
  48. package/lib/types/protocol.d.ts +34 -0
  49. package/lib/types/scopeAliases.d.ts +20 -0
  50. package/lib/types/vectorIndex.d.ts +113 -0
  51. package/lib/types/views.d.ts +33 -1
  52. package/lib/types/workflowPublish.d.ts +114 -0
  53. package/package.json +7 -7
  54. package/skills/kb-summarize/SKILL.md +158 -0
  55. package/skills/kb-summarize/i18n/zh.json +10 -0
  56. package/skills/kb-vectorize/SKILL.md +57 -0
  57. package//344/270/207/347/233/270AI/344/275/277/347/224/250/346/225/231/347/250/213.html +294 -0
  58. package/lib/types/client/KnowledgeProgressPanel.d.ts +0 -53
@@ -13,25 +13,43 @@
13
13
  */
14
14
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
15
15
  import { type KnowledgeKey } from './locales.ts';
16
+ import { type WorkflowPlazaInjected } from './workflows/WorkflowPlaza.tsx';
16
17
  declare module '@deepseek-ai/dsh-client-ui-slots' {
17
18
  interface LocaleNamespaceMap {
18
19
  'settings.knowledgeBase': KnowledgeKey;
19
20
  }
21
+ /**
22
+ * The workflow-plaza seat inside the automation-task page.
23
+ *
24
+ * That page belongs to `ui-schedule`, which this plugin cannot extend at build time
25
+ * (the running Host ships it as a built bundle). The seat is declared here as a
26
+ * type-level contract and contributed at runtime: if the deployed page does not
27
+ * declare the child slot, the registration simply never renders, and every other
28
+ * knowledge surface is unaffected.
29
+ *
30
+ * `single` = one tab id shows one body; `root` = the task page is root-scoped and a
31
+ * plaza belongs to no Session.
32
+ */
33
+ interface SlotMap {
34
+ 'schedule.manager.tabs': {
35
+ kind: 'single';
36
+ scope: 'root';
37
+ owner: Record<string, never>;
38
+ hookContext: unknown;
39
+ inject: WorkflowPlazaInjected;
40
+ };
41
+ /**
42
+ * Header actions contributed by another plugin (the DataOps notification bell).
43
+ * This plugin declares and renders the seat; it never imports the contributor.
44
+ */
45
+ 'workflow-plaza.actions': {
46
+ kind: 'single';
47
+ scope: 'root';
48
+ owner: Record<string, never>;
49
+ hookContext: unknown;
50
+ };
51
+ }
20
52
  }
21
- /**
22
- * Client services required for these contributions.
23
- *
24
- * `sessions` and `workspaces` are here because the `@`-reference dialog inserts into a
25
- * conversation draft, and that insert resolves the target session's scope through
26
- * `ctx.sessions` before it can write anything. Without the injection the service is not
27
- * on this plugin's ctx at all: the call throws `cannot get property "sessions" without
28
- * inject`, `referenceInChat` catches it into a `console.warn`, and the reader sees a
29
- * dialog that closes and inserts nothing -- the exact "clicked, nothing happened"
30
- * symptom this injection fixes at the root rather than by reporting the failure.
31
- *
32
- * `workspaces` is the other half: the mention is spelled relative to the workspace that
33
- * owns the session, so the insert needs that root to project the path.
34
- */
35
53
  export declare const inject: string[];
36
54
  /**
37
55
  * Register the knowledge-base navigation entry and Settings section.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * In-place narrowing of a listing by row name.
3
+ *
4
+ * Kept as a pure function, out of the component, because the component cannot be mounted
5
+ * in a unit test (it pulls the UI primitives and the workspace path helpers) and the rules
6
+ * below are the part that is easy to get wrong: case folding, substring vs prefix, and —
7
+ * most of all — keeping a hit reachable from the root.
8
+ *
9
+ * @module dsh-better-sidebar/client/listing-filter
10
+ */
11
+ /**
12
+ * The little a row must expose to be filtered.
13
+ *
14
+ * Only the NAME is required: callers key their rows differently (the explorer by absolute
15
+ * path), so the child lookup is passed in as {@link childrenOf} rather than assumed to be
16
+ * a field name.
17
+ */
18
+ export interface FilterableEntry {
19
+ /** Base name, what the reader scans. */
20
+ name: string;
21
+ /** Whether the row opens a level. */
22
+ isDir: boolean;
23
+ }
24
+ /** Resolve one directory row's loaded children, if that level has been read. */
25
+ export type ChildrenOf<T extends FilterableEntry> = (entry: T) => readonly T[] | undefined;
26
+ /**
27
+ * Whether a name matches the reader's term.
28
+ *
29
+ * Substring, case-insensitive, on the NAME only: this is the tree's in-place narrowing,
30
+ * and reading file bodies is what the cross-directory search is for.
31
+ * @param name - the row's base name.
32
+ * @param needle - the term, already trimmed and lower-cased by {@link normaliseNeedle}.
33
+ * @returns Whether the row itself matches.
34
+ */
35
+ export declare function nameMatches(name: string, needle: string): boolean;
36
+ /**
37
+ * Prepare the reader's raw input for matching.
38
+ *
39
+ * Trimmed because a trailing space is invisible and would otherwise make every term match
40
+ * nothing; lower-cased once here rather than once per row.
41
+ * @param raw - what the field holds.
42
+ * @returns The term to compare against.
43
+ */
44
+ export declare function normaliseNeedle(raw: string): string;
45
+ /**
46
+ * Narrow one level, recursively, keeping every hit reachable.
47
+ *
48
+ * A directory is kept when it matches, when anything under it matches, or when an ANCESTOR
49
+ * matched — the last case is what makes typing a folder name show the folder's contents
50
+ * instead of an empty row. A deeply nested hit therefore drags its whole chain with it,
51
+ * which is the only way a file inside a collapsed folder can be seen.
52
+ * @param rows - this level's rows, in display order.
53
+ * @param childrenOf - one directory row's loaded children; `undefined` means "not loaded".
54
+ * @param needle - the term, already normalised.
55
+ * @param withinMatch - whether an ancestor already matched the term.
56
+ * @returns The rows to draw at this level.
57
+ */
58
+ export declare function filterLevel<T extends FilterableEntry>(rows: readonly T[], childrenOf: ChildrenOf<T>, needle: string, withinMatch?: boolean): T[];
@@ -14,6 +14,8 @@ export declare const en: {
14
14
  readonly storageModeIntro: "Storage location";
15
15
  readonly storageModeLocal: "Local";
16
16
  readonly storageModeRemote: "Remote (NFS)";
17
+ readonly storageRoot: "Knowledge storage path";
18
+ readonly storageRootHint: "Authoritative directory (default ~/kb). Each workspace gets a .kb view of hardlinks; the two paths must be on the same filesystem.";
17
19
  readonly storageModeLocalHint: "Stored in the workspace. No mount or sync.";
18
20
  readonly storageModeRemoteHint: "Documents live in a shared NFS view that is synchronized from the authoritative tree. Requires a mount and a storage host.";
19
21
  readonly connection: "Connection";
@@ -342,10 +344,94 @@ export declare const en: {
342
344
  readonly 'step.sshfs-mount': "Mount the share over SSHFS";
343
345
  readonly 'step.sshfs-verify': "Verify the SSHFS mount";
344
346
  readonly deployLocalOnly: "Deployment steps run on a remote host. This workspace stores documents locally, so there is nothing to deploy.";
347
+ readonly sortByMtime: "By modified time";
348
+ readonly sortByName: "By name";
349
+ readonly sortBySize: "By size";
350
+ readonly sortByType: "By type";
351
+ readonly sortHeading: "Sort";
352
+ readonly filterHeading: "Filter";
353
+ readonly actionSortAndFilter: "Sort and filter";
354
+ readonly progressAlreadyMarkdown: "Already Markdown — no extraction needed";
355
+ readonly colActions: "Actions";
356
+ readonly progressIdle: "Nothing is uploading or extracting right now.";
357
+ readonly progressBadge: "{n} pending";
358
+ readonly progressBadgeMore: "99+";
359
+ readonly progressOpen: "Upload and extraction progress";
360
+ readonly progressClose: "Close";
345
361
  readonly dropRefusedNoCaller: "Cannot place this upload";
346
362
  readonly dropRefusedNoCallerReason: "The host has not reported your account yet, so the personal folder cannot be resolved. Reload and try again.";
347
363
  readonly progressResize: "Drag to resize";
348
364
  readonly shareEmpty: "Nothing has been shared yet.";
365
+ readonly workflowPlaza: "Workflow plaza";
366
+ readonly workflowPlazaTab: "Plaza";
367
+ readonly workflowReviewTab: "Review";
368
+ readonly workflowReviewEmpty: "No workflows waiting for review.";
369
+ readonly workflowReviewLoading: "Loading review queue…";
370
+ readonly workflowReviewFailed: "Review action failed; try again.";
371
+ readonly workflowReviewAuthor: "Author";
372
+ readonly workflowReviewCreated: "Submitted";
373
+ readonly workflowReviewPrompt: "View prompt";
374
+ readonly workflowReviewNote: "Rejection reason (optional)";
375
+ readonly workflowReviewApprove: "Approve";
376
+ readonly workflowReviewReject: "Reject";
377
+ readonly workflowMineTab: "My shared";
378
+ readonly workflowMineEmpty: "You have not shared a workflow yet.";
379
+ readonly workflowMineUpdated: "Updated";
380
+ readonly workflowMineReviewNote: "Review note";
381
+ readonly workflowMineResubmit: "Resubmit";
382
+ readonly workflowMineUnpublish: "Unpublish";
383
+ readonly workflowMineDetails: "View details";
384
+ readonly workflowMineActionFailed: "Action failed; try again.";
385
+ readonly workflowMineStatusPending: "In review";
386
+ readonly workflowMineStatusApproved: "Shared";
387
+ readonly workflowMineStatusRejected: "Rejected";
388
+ readonly workflowMineStatusUnpublished: "Unpublished";
389
+ readonly workflowMineScopeGlobal: "Global";
390
+ readonly workflowMineScopePrivate: "Private";
391
+ readonly workflowMineScopeDepartment: "{name} department";
392
+ readonly workflowSubscribe: "Subscribe";
393
+ readonly workflowSubscribed: "Subscribed";
394
+ readonly workflowUnsubscribe: "Unsubscribe";
395
+ readonly workflowFrequency: "Frequency";
396
+ readonly workflowModel: "Model";
397
+ readonly workflowModelLoading: "Loading models…";
398
+ readonly workflowModelUnavailable: "Default model (catalog unavailable)";
399
+ readonly workflowModelDefault: "Workspace default model";
400
+ readonly workflowEffort: "Reasoning effort";
401
+ readonly workflowDaily: "Daily";
402
+ readonly workflowWeekly: "Weekly";
403
+ readonly workflowMonthly: "Monthly";
404
+ readonly workflowCustom: "Custom";
405
+ readonly workflowEveryHours: "Every N hours";
406
+ readonly workflowEveryMinutes: "Every N minutes";
407
+ readonly workflowHoursCount: "Hours";
408
+ readonly workflowMinutesCount: "Minutes";
409
+ readonly workflowIntervalInvalid: "Enter a whole number between {min} and {max}";
410
+ readonly workflowIntervalUnitHours: "hour(s)";
411
+ readonly workflowIntervalUnitMinutes: "minute(s)";
412
+ readonly workflowCronLabel: "Cron expression";
413
+ readonly workflowCronInvalid: "Enter a 5-field cron (minute hour day month weekday)";
414
+ readonly workflowSubscribing: "Subscribing…";
415
+ readonly workflowSubscribeFailed: "Subscribe failed: {reason}";
416
+ readonly workflowNoSchedules: "No workflows subscribed yet";
417
+ readonly workflowDialogTitle: "Subscribe to this workflow";
418
+ readonly workflowDialogConfirm: "Create task";
419
+ readonly workflowDialogCancel: "Cancel";
420
+ readonly workflowRetry: "Retry";
421
+ readonly workflowAlreadySubscribed: "Already subscribed";
422
+ readonly workflowOrganizeTitle: "Tidy up my personal knowledge base";
423
+ readonly workflowOrganizeDescription: "Groups personal documents into topic folders and writes an index for each level, so the bucket stays readable as it grows.";
424
+ readonly workflowIndexTitle: "Tidy document text copies";
425
+ readonly workflowIndexDescription: "Scans personal documents and fills in the Markdown text copies that are still missing, so every file can be read as text.";
426
+ readonly workflowArchiveTitle: "Archive stale personal documents";
427
+ readonly workflowArchiveDescription: "Moves documents untouched for 90 days into an Archive folder and marks them as archived in the index.";
428
+ readonly workflowTaskDetail: "Task detail";
429
+ readonly workflowReasonAlready: "this workflow already has a task";
430
+ readonly workflowReasonModel: "the selected model is unavailable — choose another model";
431
+ readonly workflowReasonSession: "the session ended with an error — open it to see the reason";
432
+ readonly workflowReasonPrompt: "the instruction could not be sent";
433
+ readonly workflowReasonNotCreated: "the task has not appeared yet — it may still be creating; refresh in a moment";
434
+ readonly workflowReasonCancel: "the task could not be removed";
349
435
  readonly shareGroupReceived: "Inbound";
350
436
  readonly shareGroupGiven: "Outbound";
351
437
  readonly shareColRelation: "Sharing";
@@ -396,9 +482,16 @@ export declare const en: {
396
482
  readonly progressUploading: "Uploading {done}/{total}";
397
483
  readonly progressPending: "Queued";
398
484
  readonly progressExtracting: "Extracting";
485
+ readonly progressStepParse: "Parsing document";
486
+ readonly progressStepChunk: "Chunking text";
487
+ readonly progressStepIndex: "Writing FTS5 index";
488
+ readonly progressStepDone: "Finishing";
399
489
  readonly progressExtractingVia: "Extracting ({via})";
400
490
  readonly progressSucceeded: "Done";
401
491
  readonly progressFailed: "Failed: {reason}";
492
+ readonly progressFailedTitle: "Failed";
493
+ readonly progressRawReason: "Raw error: {reason}";
494
+ readonly extractReasonMissingFile: "The source file no longer exists or has moved";
402
495
  readonly progressUnsupported: "This file type has no extractor";
403
496
  readonly progressRetry: "Retry";
404
497
  readonly progressCollapse: "Collapse";
@@ -417,7 +510,33 @@ export declare const en: {
417
510
  readonly settingsMineruHint: "Where documents are parsed. Leave it empty to use the port this deployment was configured with.";
418
511
  readonly settingsMineruPlaceholder: "Leave empty to derive from the configured port";
419
512
  readonly settingsMineruDerived: "Currently derived from the configured port.";
513
+ readonly vectorHeading: "Vector retrieval";
514
+ readonly vectorHint: "Optional hybrid recall: FTS remains active; vectors only add semantic candidates. When disabled, search behaviour is unchanged and no embedding provider is called.";
515
+ readonly vectorEnabled: "Enable vector retrieval";
516
+ readonly vectorEnabledOn: "On: FTS and vector candidates are fused with RRF.";
517
+ readonly vectorEnabledOff: "Off: pure FTS, no embedding calls.";
518
+ readonly vectorProvider: "Provider";
519
+ readonly vectorBaseUrl: "Base URL";
520
+ readonly vectorModel: "Embedding model";
521
+ readonly vectorDimensions: "Dimensions";
522
+ readonly vectorCredentialRef: "API key credential reference";
523
+ readonly vectorCredentialHint: "A credential name only; the key is never stored or shown here.";
524
+ readonly vectorRrfK: "RRF k";
525
+ readonly vectorCandidateK: "Candidate K";
526
+ readonly vectorWeights: "RRF weights (FTS / vector)";
527
+ readonly vectorFtsWeight: "FTS weight";
528
+ readonly vectorVectorWeight: "Vector weight";
529
+ readonly vectorWeightsHint: "FTS weight 1.0 and vector weight 0.5 by default. Vector-only hits cannot outrank strong FTS hits.";
530
+ readonly vectorCoverage: "Embedded {embedded} / {total} chunks; failed {failed}.";
531
+ readonly vectorProgress: "Vectorizing {done} / {total} documents...";
532
+ readonly vectorRebuild: "Rebuild vector index";
533
+ readonly vectorRebuildHint: "Whole-library rebuild; asks for confirmation. Use kb-vectorize for one document or directory.";
534
+ readonly vectorRebuildConfirm: "Rebuild vectors for the whole knowledge base? This calls the embedding provider for every current chunk.";
535
+ readonly vectorBadNumber: "Vector settings contain an invalid number.";
420
536
  readonly settingsMineruExplicit: "Currently using the address above.";
537
+ readonly settingsMineruShared: "Currently using the address the 「Document recognition」 plugin configured for this deployment.";
538
+ readonly settingsMineruNone: "No parser address is available. Ask an administrator to configure one in 「Document recognition」.";
539
+ readonly settingsMineruSharedInvalid: "The address saved here is not a valid address, so a fallback is in use.";
421
540
  readonly settingsSave: "Save";
422
541
  readonly settingsSaving: "Saving…";
423
542
  readonly ownerGlobal: "Everyone";
@@ -485,6 +604,10 @@ export declare const en: {
485
604
  readonly createDestinationPicker: "Location";
486
605
  readonly createDestination: "Destination";
487
606
  readonly createAtRoot: "Top level";
607
+ readonly createStepBack: "Back";
608
+ readonly createDropTitle: "Drop files here, or click to choose";
609
+ readonly createSelectedCount: "Selected {n} files";
610
+ readonly createRemoveFile: "Remove {name}";
488
611
  readonly createAtUp: "Up one level";
489
612
  readonly createLoading: "Loading folders…";
490
613
  readonly createNoSubfolders: "No folders inside this one.";
@@ -23,7 +23,7 @@
23
23
  * @module @sparkelf/dsh-knowledge-base/client
24
24
  */
25
25
  /** Outcome of asking the host to show a document. */
26
- export type OpenOutcome = 'opened' | 'no-context' | 'no-registry' | 'unclaimed' | 'no-column';
26
+ export type OpenOutcome = 'opened' | 'no-seat' | 'no-context' | 'no-registry' | 'unclaimed' | 'no-column';
27
27
  /**
28
28
  * Ask the host to open a document in its right-hand file window.
29
29
  *
@@ -35,7 +35,7 @@ export type OpenOutcome = 'opened' | 'no-context' | 'no-registry' | 'unclaimed'
35
35
  * @returns Why it did or did not open; the caller reports the failure rather than
36
36
  * appearing to do nothing.
37
37
  */
38
- export declare function openInSidebar(ctx: unknown, sessionId: string | undefined, cwd: string | undefined, absolutePath: string): OpenOutcome;
38
+ export declare function openInSidebar(ctx: unknown, sessionId: string | undefined, cwd: string | undefined, absolutePath: string, ensureSession?: () => Promise<string | undefined>): Promise<OpenOutcome>;
39
39
  /**
40
40
  * Return the right column's width to the page.
41
41
  *
@@ -35,20 +35,201 @@ export declare function failureReason(body: {
35
35
  error?: unknown;
36
36
  code?: unknown;
37
37
  } | undefined, status: number): string;
38
- /** The strip's height bounds, in pixels. */
39
- export declare const PROGRESS_MIN_HEIGHT = 44;
40
- export declare const PROGRESS_DEFAULT_HEIGHT = 180;
41
- /** The share of the panel's height the strip may take, leaving the tree the rest. */
42
- export declare const PROGRESS_MAX_SHARE = 0.6;
43
- /**
44
- * Clamp a dragged height into the strip's legal range.
45
- *
46
- * The upper bound is a share of the PANEL, not a constant: the strip must never grow
47
- * until the file tree is unreachable, and how much room that is depends on how tall the
48
- * panel currently is. A 600px panel and a 1200px panel therefore allow different
49
- * maximums, which is what keeps the tree visible on both.
50
- * @param desired - the height the pointer asked for.
51
- * @param panelHeight - the panel's own height, when known.
52
- * @returns The height to use.
53
- */
54
- export declare function clampProgressHeight(desired: number, panelHeight: number | undefined): number;
38
+ /** How a group of tasks breaks down, with every task counted exactly once. */
39
+ export interface TaskTally {
40
+ /** Tasks that produced what the reader asked for. */
41
+ done: number;
42
+ /** Tasks that ended without producing it, for any reason. */
43
+ failed: number;
44
+ /** Tasks inside an unsupported format, kept apart from ordinary failures. */
45
+ unsupported: number;
46
+ /** Tasks still queued or running. */
47
+ active: number;
48
+ /** Every task in the group. */
49
+ total: number;
50
+ }
51
+ /**
52
+ * Count a group of extraction tasks.
53
+ *
54
+ * The invariant this exists for: `done + failed + active === total`. A tally that drops a
55
+ * state makes the summary read "0 extracted, 0 failed" beside a list holding one row --
56
+ * the reader sees a task that neither succeeded nor failed and has no way to learn what
57
+ * happened to it.
58
+ *
59
+ * `unsupported` is counted in BOTH its own field and in `failed`. The two audiences are
60
+ * different: the summary groups it with failures (a reader who did not get their Markdown
61
+ * does not care why), while the row still says "this file type has no extractor", which is
62
+ * where the specific reason belongs.
63
+ * @param tasks - the tasks to count.
64
+ * @returns The breakdown.
65
+ */
66
+ export declare function tallyExtraction(tasks: readonly {
67
+ state: string;
68
+ }[]): TaskTally;
69
+ /**
70
+ * Count a group of upload tasks.
71
+ *
72
+ * Same invariant as {@link tallyExtraction}, for the upload batch: every row is exactly
73
+ * one of done, failed, or still in flight.
74
+ * @param tasks - the upload rows.
75
+ * @returns The breakdown.
76
+ */
77
+ export declare function tallyUploads(tasks: readonly {
78
+ state: string;
79
+ }[]): TaskTally;
80
+ /** One upload row, as the notification panel shows it. */
81
+ export interface UploadTaskView {
82
+ /** The file's path relative to the drop target: the name the reader dropped. */
83
+ name: string;
84
+ /** The tree-relative directory it is landing in, `''` for the mount root. */
85
+ dir: string;
86
+ /** Where this file is in its own lifecycle. */
87
+ state: 'waiting' | 'uploading' | 'done' | 'failed';
88
+ /** Why it failed, in the Host's own words. */
89
+ reason?: string;
90
+ /**
91
+ * Bytes sent, as 0-100.
92
+ *
93
+ * Absent while the browser cannot know it (a body without a known length) and on every
94
+ * settled row: a percentage that never moves, or a track left behind after completion,
95
+ * both claim something that is not true.
96
+ */
97
+ percent?: number;
98
+ }
99
+ /**
100
+ * How many tasks are still worth the reader's attention.
101
+ *
102
+ * This is the notification badge's number: uploads that have not settled, plus extractions
103
+ * the Host is still queued on or running. Settled work is deliberately excluded -- a badge
104
+ * that still shows a count after everything finished trains the reader to ignore it, and
105
+ * the reason to open the panel is then gone.
106
+ *
107
+ * `unsupported` is settled too: no extractor exists for that format, so waiting on it
108
+ * would leave the badge lit forever.
109
+ * @param uploads - the current upload batch.
110
+ * @param extractions - the extraction tasks the Host reports.
111
+ * @returns The number of tasks still in flight.
112
+ */
113
+ export declare function pendingCount(uploads: readonly {
114
+ state: string;
115
+ }[], extractions: readonly {
116
+ state: string;
117
+ }[]): number;
118
+ /**
119
+ * How long an optimistic extraction entry may wait for the Host to confirm it.
120
+ *
121
+ * A request can fail in ways the browser never reports (a dropped connection, a proxy that
122
+ * swallows the body), so an entry added on click must expire on its own. Thirty seconds is
123
+ * longer than a healthy `extract.status` round trip and short enough that a reader is not
124
+ * left staring at a task that will never exist.
125
+ */
126
+ export declare const OPTIMISTIC_TTL_MS = 30000;
127
+ /**
128
+ * One row the reader asked for that the Host has not reported back yet.
129
+ *
130
+ * Kept separate from `ExtractionTaskView` on purpose: a server task has an authoritative
131
+ * state, and this has only a request that was sent. Merging them would let a guess outlive
132
+ * the truth.
133
+ */
134
+ export interface OptimisticExtraction {
135
+ /** The document the reader asked to extract. */
136
+ relPath: string;
137
+ /** When the request was issued, in milliseconds. */
138
+ requestedAt: number;
139
+ }
140
+ /**
141
+ * Whether an optimistic entry is still waiting for the Host to mention it.
142
+ * @param entry - the pending request.
143
+ * @param now - the current time in milliseconds.
144
+ * @returns Whether it is still within its lifetime.
145
+ */
146
+ export declare function optimisticAlive(entry: OptimisticExtraction, now: number): boolean;
147
+ /**
148
+ * The entries the panel should still show as "queued".
149
+ *
150
+ * An entry is dropped for either of two reasons, and the distinction matters to the reader:
151
+ *
152
+ * - the Host has reported that document, so the authoritative row replaces the guess;
153
+ * - it has been waiting longer than {@link OPTIMISTIC_TTL_MS}, so the task was never
154
+ * created and the panel has to stop claiming otherwise.
155
+ * @param pending - the optimistic entries.
156
+ * @param reported - the paths the Host currently reports.
157
+ * @param now - the current time in milliseconds.
158
+ * @returns The entries still worth showing.
159
+ */
160
+ export declare function survivingOptimistic(pending: readonly OptimisticExtraction[], reported: readonly {
161
+ relPath: string;
162
+ }[], now: number): OptimisticExtraction[];
163
+ /**
164
+ * The rows the panel draws, with optimistic entries folded in.
165
+ *
166
+ * A path the Host reports ALWAYS wins: the guess exists only to cover the gap before the
167
+ * first `extract.status` answer, and showing both would read as two tasks.
168
+ * @param reported - what the Host reports, in its own order.
169
+ * @param pending - the optimistic entries, oldest first.
170
+ * @param now - the current time in milliseconds.
171
+ * @returns One row per document, plus the rows only the guest knows about.
172
+ */
173
+ export declare function mergeOptimistic<T extends {
174
+ relPath: string;
175
+ }>(reported: readonly T[], pending: readonly OptimisticExtraction[], now: number): {
176
+ rows: T[];
177
+ queued: OptimisticExtraction[];
178
+ };
179
+ /**
180
+ * Turn a failed extraction request into a sentence a reader can act on.
181
+ *
182
+ * The menu's entry used to discard the response entirely, which is why a failed click
183
+ * looked identical to a successful one. Each status here means something different about
184
+ * what to do next, so they are not collapsed into "failed".
185
+ * @param status - the HTTP status, or 0 when the request never completed.
186
+ * @param body - the parsed body, when it was JSON.
187
+ * @returns A reason for the reader.
188
+ */
189
+ export declare function extractionFailureReason(status: number, body: {
190
+ error?: unknown;
191
+ message?: unknown;
192
+ } | undefined): string;
193
+ /** A failed extraction the panel knows about even when the Host has no task for it. */
194
+ export interface FailedExtractionView {
195
+ /** Knowledge-relative path of the SOURCE document, as the request named it. */
196
+ relPath: string;
197
+ /** The raw reason, exactly as the Host or the transport reported it. */
198
+ reason: string;
199
+ /** When the failure was observed, in milliseconds. */
200
+ at: number;
201
+ }
202
+ /** Why an extraction produced nothing, as far as the reader is concerned. */
203
+ export type ExtractionFailureKind =
204
+ /** The source document is no longer at the path the tree showed. */
205
+ 'missing-file'
206
+ /** Anything else: keep the Host's own wording. */
207
+ | 'other';
208
+ /**
209
+ * Classify one raw extraction failure.
210
+ *
211
+ * The Host answers `no such file` when the authoritative path it was given has no file.
212
+ * That string is the server's own words, not a sentence a reader can act on, so it is
213
+ * the one case with a dedicated translation; every other reason is shown verbatim
214
+ * because inventing a friendlier sentence for an unknown failure would hide the detail
215
+ * the operator needs.
216
+ * @param reason - the raw reason from the Host or transport.
217
+ * @returns The kind the UI maps to localized copy.
218
+ */
219
+ export declare function classifyExtractionFailure(reason: string): ExtractionFailureKind;
220
+ /**
221
+ * The client-side failure rows the panel should draw.
222
+ *
223
+ * A path the Host already reports always wins: after a retry the Host's own task is the
224
+ * authority, and showing both would read as two failures for one document. The same rule
225
+ * applies to a queued guess -- the click is newer than the recorded failure.
226
+ * @param reported - the tasks the Host reports.
227
+ * @param failures - the session's recorded request failures.
228
+ * @param queued - optimistic entries still waiting for the Host.
229
+ * @returns One row per document that only the client knows failed.
230
+ */
231
+ export declare function survivingClientFailures<T extends {
232
+ relPath: string;
233
+ }>(reported: readonly T[], failures: readonly FailedExtractionView[], queued: readonly {
234
+ relPath: string;
235
+ }[]): FailedExtractionView[];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Remove knowledge-explorer tabs that were persisted with the old placeholder id.
3
+ *
4
+ * Earlier builds let `@kb-explorer` reach a session-scoped sidebar tab address. The
5
+ * tab was stored in the browser's per-session layout; after the placeholder was
6
+ * removed from the open path, the old tab still made the restored sidebar resolve a
7
+ * non-existent Session. This one-way migration drops only those tab records. Host fs
8
+ * routes still use the placeholder key and are untouched.
9
+ */
10
+ /**
11
+ * Remove persisted sidebar layouts that contain the retired explorer placeholder.
12
+ *
13
+ * Called once when the knowledge plugin activates. It is deliberately narrow: only
14
+ * tab records carrying a `session/@kb-explorer/` address are removed; all other tabs
15
+ * and per-session layout data survive byte-for-byte when nothing matches.
16
+ *
17
+ * @param storage - localStorage-like store, injectable for tests.
18
+ * @returns how many storage keys were rewritten or removed.
19
+ */
20
+ export declare function sanitizeExplorerTabs(storage?: Pick<Storage, 'length' | 'key' | 'getItem' | 'setItem' | 'removeItem'> | undefined): number;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Visibility gate for the knowledge-base Settings section.
3
+ *
4
+ * The Settings surface is an operator tool, not a reader feature: only the
5
+ * DataOps platform administrator roles may see it. The gate is deliberately a
6
+ * separate module so the rule and the register/dispose lifecycle can be tested
7
+ * without mounting React or a slot registry.
8
+ *
9
+ * The identity is presentation-only (`window.__DATAOPS_USER__`), posted by the
10
+ * portal. Authorization for actual writes still lives in DataOps; hiding the
11
+ * section is representation, and the Host routes refuse unauthorized callers
12
+ * independently.
13
+ *
14
+ * @module @sparkelf/dsh-knowledge-base/client/settingsGate
15
+ */
16
+ /** The event the account plugin dispatches after replacing `__DATAOPS_USER__`. */
17
+ export declare const DATAOPS_ACCOUNT_CHANGED_EVENT = "dataops-account:changed";
18
+ /** Roles that may see and use the knowledge-base Settings section. */
19
+ export declare const KNOWLEDGE_SETTINGS_ROLES: readonly ["超级管理员", "管理员"];
20
+ /** The presentation identity fields this gate reads. */
21
+ export interface SettingsIdentityLike {
22
+ readonly name?: string | null;
23
+ }
24
+ /** The window surface the gate reads and subscribes to. */
25
+ export interface SettingsWindowLike {
26
+ readonly __DATAOPS_USER__?: SettingsIdentityLike | null;
27
+ addEventListener(type: string, listener: () => void): void;
28
+ removeEventListener(type: string, listener: () => void): void;
29
+ }
30
+ /** Exactly the two DataOps platform roles; department admins are excluded. */
31
+ export declare function canSeeKnowledgeSettings(identity: SettingsIdentityLike | null | undefined): boolean;
32
+ /** Register one slot contribution; returns its disposer. */
33
+ export type SettingsRegister = () => () => void;
34
+ /** Options keep the gate injectable for a unit test. */
35
+ export interface SettingsGateOptions {
36
+ /** Register the section; called only while visible and not already registered. */
37
+ readonly register: SettingsRegister;
38
+ /** Read the current identity; defaults to the live window. */
39
+ readonly readIdentity?: () => SettingsIdentityLike | null | undefined;
40
+ /** Subscribe to identity changes; defaults to the window event. */
41
+ readonly subscribe?: (listener: () => void) => () => void;
42
+ /**
43
+ * Extra same-tab signal, mirroring the account bridge's dataset write. The event is
44
+ * the contract; the observer exists because a portal/account bundle can publish the
45
+ * first identity without dispatching it, and the section must appear on first paint.
46
+ */
47
+ readonly observe?: (sync: () => void) => () => void;
48
+ }
49
+ /** A started gate with an explicit sync for tests and account events. */
50
+ export interface SettingsGate {
51
+ /** Register if allowed and subscribe to future changes. */
52
+ start(): void;
53
+ /** Run the visibility rule now; register or dispose as needed. */
54
+ sync(): void;
55
+ /** Unsubscribe and dispose any live registration. */
56
+ stop(): void;
57
+ }
58
+ /**
59
+ * Build a gate around one slot registration.
60
+ *
61
+ * The gate never registers twice: an account event that leaves the role unchanged is
62
+ * a no-op, and a downgrade disposes the mounted section rather than merely hiding its
63
+ * controls. That is the "content area must also not render" requirement.
64
+ */
65
+ export declare function createSettingsGate(options: SettingsGateOptions): SettingsGate;