@sparkelf/dsh-knowledge-base 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +4266 -1970
- package/lib/client.js.map +1 -1
- package/lib/index.js +293 -12
- package/lib/types/api.d.ts +46 -0
- package/lib/types/client/KnowledgeContextMenu.d.ts +31 -6
- package/lib/types/client/KnowledgeFileTree.d.ts +10 -0
- package/lib/types/client/KnowledgeHome.d.ts +23 -0
- package/lib/types/client/KnowledgeListing.d.ts +14 -0
- package/lib/types/client/KnowledgePermissionOverview.d.ts +51 -3
- package/lib/types/client/KnowledgeProgressPanel.d.ts +53 -0
- package/lib/types/client/KnowledgeReferencePicker.d.ts +16 -0
- package/lib/types/client/KnowledgeSelectionBar.d.ts +28 -0
- package/lib/types/client/auditPaging.d.ts +40 -0
- package/lib/types/client/index.d.ts +14 -1
- package/lib/types/client/insertReadiness.d.ts +60 -0
- package/lib/types/client/locales.d.ts +68 -1
- package/lib/types/client/paging.d.ts +69 -0
- package/lib/types/client/permissionRows.d.ts +176 -5
- package/lib/types/client/progressRules.d.ts +54 -0
- package/lib/types/client/referenceTargets.d.ts +60 -0
- package/lib/types/client/shareRelations.d.ts +81 -0
- package/lib/types/client/useFileDrop.d.ts +171 -0
- package/lib/types/client/useFileDropHook.d.ts +64 -0
- package/lib/types/extractionQueue.d.ts +154 -0
- package/lib/types/index.d.ts +53 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -2130,6 +2130,144 @@ function sshManagerRunner(options) {
|
|
|
2130
2130
|
}
|
|
2131
2131
|
};
|
|
2132
2132
|
}
|
|
2133
|
+
/**
|
|
2134
|
+
* Create an extraction queue.
|
|
2135
|
+
*
|
|
2136
|
+
* @param options - the runner and the predicate that decides whether a path has a reader.
|
|
2137
|
+
* @returns a queue that runs at most {@link MAX_CONCURRENT_EXTRACTIONS} task at a time.
|
|
2138
|
+
*/
|
|
2139
|
+
function createExtractionQueue(options) {
|
|
2140
|
+
/** Tasks by source path, in submission order. */
|
|
2141
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
2142
|
+
/** Work waiting for the single worker slot. */
|
|
2143
|
+
const waiting = [];
|
|
2144
|
+
/** Resolvers waiting for the queue to drain. */
|
|
2145
|
+
let idleWaiters = [];
|
|
2146
|
+
/**
|
|
2147
|
+
* Resolvers waiting for ONE path's task to become terminal, keyed by that path.
|
|
2148
|
+
*
|
|
2149
|
+
* Per-task rather than a single queue-wide signal: a retry must not be woken by an
|
|
2150
|
+
* unrelated upload finishing, and a caller that registered a waiter must receive the
|
|
2151
|
+
* terminal state of its own task even if the bound evicts the record afterwards.
|
|
2152
|
+
*/
|
|
2153
|
+
const taskWaiters = /* @__PURE__ */ new Map();
|
|
2154
|
+
let running;
|
|
2155
|
+
let started = false;
|
|
2156
|
+
/** Forget the oldest finished task once the bound is exceeded. */
|
|
2157
|
+
function evictFinished() {
|
|
2158
|
+
if (tasks.size <= 200) return;
|
|
2159
|
+
for (const [relPath, task] of tasks) {
|
|
2160
|
+
if (task.state === "pending" || task.state === "running") continue;
|
|
2161
|
+
tasks.delete(relPath);
|
|
2162
|
+
if (tasks.size <= 200) return;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
/** Resolve everyone waiting on an idle queue. */
|
|
2166
|
+
function settleIdle() {
|
|
2167
|
+
if (running !== void 0 || waiting.length > 0) return;
|
|
2168
|
+
const waiters = idleWaiters;
|
|
2169
|
+
idleWaiters = [];
|
|
2170
|
+
for (const resolve of waiters) resolve();
|
|
2171
|
+
}
|
|
2172
|
+
/** Run one task, recording its terminal state. */
|
|
2173
|
+
async function runOne(item) {
|
|
2174
|
+
const task = tasks.get(item.relPath);
|
|
2175
|
+
if (task === void 0) return;
|
|
2176
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2177
|
+
task.state = "running";
|
|
2178
|
+
task.startedAt = startedAt;
|
|
2179
|
+
running = item.relPath;
|
|
2180
|
+
try {
|
|
2181
|
+
const outcome = await options.run(item.relPath, item.mineruBaseUrl);
|
|
2182
|
+
task.state = outcome.state;
|
|
2183
|
+
if (outcome.markdown !== void 0) task.markdown = outcome.markdown;
|
|
2184
|
+
if (outcome.via !== void 0) task.via = outcome.via;
|
|
2185
|
+
if (outcome.reason !== void 0) task.reason = outcome.reason;
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
task.state = "failed";
|
|
2188
|
+
task.reason = error instanceof Error ? error.message : String(error);
|
|
2189
|
+
} finally {
|
|
2190
|
+
task.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2191
|
+
running = void 0;
|
|
2192
|
+
const waiters = taskWaiters.get(item.relPath);
|
|
2193
|
+
if (waiters !== void 0) {
|
|
2194
|
+
taskWaiters.delete(item.relPath);
|
|
2195
|
+
const terminal = { ...task };
|
|
2196
|
+
for (const resolve of waiters) resolve(terminal);
|
|
2197
|
+
}
|
|
2198
|
+
evictFinished();
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
/** Pull work until the queue is empty, then let idle waiters go. */
|
|
2202
|
+
async function work() {
|
|
2203
|
+
if (started) return;
|
|
2204
|
+
started = true;
|
|
2205
|
+
try {
|
|
2206
|
+
while (waiting.length > 0) {
|
|
2207
|
+
const item = waiting.shift();
|
|
2208
|
+
if (item === void 0) break;
|
|
2209
|
+
if (!tasks.has(item.relPath)) continue;
|
|
2210
|
+
await runOne(item);
|
|
2211
|
+
}
|
|
2212
|
+
} finally {
|
|
2213
|
+
started = false;
|
|
2214
|
+
running = void 0;
|
|
2215
|
+
settleIdle();
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
return {
|
|
2219
|
+
enqueue(relPath, mineruBaseUrl) {
|
|
2220
|
+
const existing = tasks.get(relPath);
|
|
2221
|
+
if (existing !== void 0 && (existing.state === "pending" || existing.state === "running")) return { ...existing };
|
|
2222
|
+
const task = {
|
|
2223
|
+
relPath,
|
|
2224
|
+
state: options.isExtractable(relPath) ? "pending" : "unsupported",
|
|
2225
|
+
queuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2226
|
+
};
|
|
2227
|
+
if (task.state === "pending") waiting.push({
|
|
2228
|
+
relPath,
|
|
2229
|
+
...mineruBaseUrl === void 0 ? {} : { mineruBaseUrl }
|
|
2230
|
+
});
|
|
2231
|
+
else {
|
|
2232
|
+
task.reason = "this file type has no extractor";
|
|
2233
|
+
task.finishedAt = task.queuedAt;
|
|
2234
|
+
}
|
|
2235
|
+
tasks.set(relPath, task);
|
|
2236
|
+
evictFinished();
|
|
2237
|
+
if (task.state === "pending") Promise.resolve().then(() => work());
|
|
2238
|
+
return { ...task };
|
|
2239
|
+
},
|
|
2240
|
+
get(relPath) {
|
|
2241
|
+
const task = tasks.get(relPath);
|
|
2242
|
+
return task === void 0 ? void 0 : { ...task };
|
|
2243
|
+
},
|
|
2244
|
+
snapshot() {
|
|
2245
|
+
const visible = [];
|
|
2246
|
+
for (const task of tasks.values()) visible.push({ ...task });
|
|
2247
|
+
return {
|
|
2248
|
+
running,
|
|
2249
|
+
size: waiting.length + (running === void 0 ? 0 : 1),
|
|
2250
|
+
tasks: visible
|
|
2251
|
+
};
|
|
2252
|
+
},
|
|
2253
|
+
settled(relPath) {
|
|
2254
|
+
const task = tasks.get(relPath);
|
|
2255
|
+
if (task === void 0) return Promise.resolve(void 0);
|
|
2256
|
+
if (task.state !== "pending" && task.state !== "running") return Promise.resolve({ ...task });
|
|
2257
|
+
return new Promise((resolve) => {
|
|
2258
|
+
const pending = taskWaiters.get(relPath) ?? [];
|
|
2259
|
+
pending.push((value) => resolve(value));
|
|
2260
|
+
taskWaiters.set(relPath, pending);
|
|
2261
|
+
});
|
|
2262
|
+
},
|
|
2263
|
+
drain() {
|
|
2264
|
+
if (running === void 0 && waiting.length === 0) return Promise.resolve();
|
|
2265
|
+
return new Promise((resolve) => {
|
|
2266
|
+
idleWaiters.push(resolve);
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2133
2271
|
//#endregion
|
|
2134
2272
|
//#region src/extract.ts
|
|
2135
2273
|
/**
|
|
@@ -3573,9 +3711,13 @@ async function fetchPrincipals(ctx, config, kind) {
|
|
|
3573
3711
|
const uuid = typeof row.id === "string" ? row.id : void 0;
|
|
3574
3712
|
const id = kind === "user" ? uuid : code;
|
|
3575
3713
|
if (id === void 0) return void 0;
|
|
3714
|
+
const name = typeof row.displayName === "string" ? row.displayName : typeof row.name === "string" ? row.name : typeof row.username === "string" ? row.username : id;
|
|
3715
|
+
const roleCodes = kind === "user" && Array.isArray(row.roles) ? row.roles.map((role) => role.code).filter((code) => typeof code === "string" && code !== "") : void 0;
|
|
3576
3716
|
return {
|
|
3577
3717
|
id,
|
|
3578
|
-
label:
|
|
3718
|
+
label: name,
|
|
3719
|
+
...kind !== "user" || typeof row.username !== "string" ? {} : { username: row.username },
|
|
3720
|
+
...roleCodes === void 0 ? {} : { roleCodes }
|
|
3579
3721
|
};
|
|
3580
3722
|
}).filter((row) => row !== void 0);
|
|
3581
3723
|
}
|
|
@@ -3950,6 +4092,42 @@ async function extractBeside(config, relPath, mineruUrl) {
|
|
|
3950
4092
|
};
|
|
3951
4093
|
}
|
|
3952
4094
|
/**
|
|
4095
|
+
* Translate {@link extractBeside}'s answer into a queue terminal state.
|
|
4096
|
+
*
|
|
4097
|
+
* The extractor reports three different outcomes through two fields, and they mean
|
|
4098
|
+
* different things to a reader deciding what to do next:
|
|
4099
|
+
*
|
|
4100
|
+
* - `markdown` set, no `reason` — the sibling was written (`succeeded`).
|
|
4101
|
+
* - `markdown` set WITH a `reason` — the text was produced but writing the sibling was
|
|
4102
|
+
* refused, typically because it already exists. The reason is kept so a retry can
|
|
4103
|
+
* report why nothing changed (`failed`, with the refusal verbatim).
|
|
4104
|
+
* - `markdown` empty, `reason` set — no authority could read the document; this is the
|
|
4105
|
+
* `no-extractor-succeeded: …` case a reader retries after an upgrade.
|
|
4106
|
+
*
|
|
4107
|
+
* An `undefined` answer means the format has no extractor at all, which the queue
|
|
4108
|
+
* normally prevents by asking `isExtractable` first; it is mapped for the caller that
|
|
4109
|
+
* bypassed that gate rather than left to throw.
|
|
4110
|
+
*
|
|
4111
|
+
* @param outcome - what {@link extractBeside} returned.
|
|
4112
|
+
* @returns the state the queue records.
|
|
4113
|
+
*/
|
|
4114
|
+
function extractionOutcomeOf(outcome) {
|
|
4115
|
+
if (outcome === void 0) return {
|
|
4116
|
+
state: "unsupported",
|
|
4117
|
+
reason: "this file type has no extractor"
|
|
4118
|
+
};
|
|
4119
|
+
if (outcome.markdown !== "" && outcome.reason === void 0) return {
|
|
4120
|
+
state: "succeeded",
|
|
4121
|
+
markdown: outcome.markdown,
|
|
4122
|
+
...outcome.via === void 0 ? {} : { via: outcome.via }
|
|
4123
|
+
};
|
|
4124
|
+
return {
|
|
4125
|
+
state: "failed",
|
|
4126
|
+
...outcome.markdown === "" ? {} : { markdown: outcome.markdown },
|
|
4127
|
+
...outcome.reason === void 0 ? {} : { reason: outcome.reason }
|
|
4128
|
+
};
|
|
4129
|
+
}
|
|
4130
|
+
/**
|
|
3953
4131
|
* MinerU's base URL for this deployment, or undefined when it is not configured.
|
|
3954
4132
|
*
|
|
3955
4133
|
* @param config - resolved plugin config.
|
|
@@ -4313,6 +4491,22 @@ function apply(ctx, deploymentConfig) {
|
|
|
4313
4491
|
/** Completed and in-flight step runs, newest last. */
|
|
4314
4492
|
const runs = [];
|
|
4315
4493
|
/**
|
|
4494
|
+
* Background extraction, one worker for this deployment.
|
|
4495
|
+
*
|
|
4496
|
+
* Upload and extraction are separate promises (see `extractionQueue.ts`): the bytes
|
|
4497
|
+
* must be visible when the upload answers, while the `.md` sibling may take a minute
|
|
4498
|
+
* on a scanned PDF. `run` reuses the same permission-checked `extractBeside` path the
|
|
4499
|
+
* upload used, so a background run and a manual retry cannot diverge. One queue per
|
|
4500
|
+
* registration means one worker per process, which is what keeps two parses from
|
|
4501
|
+
* competing for the same small box.
|
|
4502
|
+
*/
|
|
4503
|
+
const extractionQueue = createExtractionQueue({
|
|
4504
|
+
isExtractable: (relPath) => isExtractable(relPath),
|
|
4505
|
+
run: async (relPath, mineruBaseUrl) => {
|
|
4506
|
+
return extractionOutcomeOf(await extractBeside(config, relPath, mineruBaseUrl));
|
|
4507
|
+
}
|
|
4508
|
+
});
|
|
4509
|
+
/**
|
|
4316
4510
|
* Callers whose missing pointer has already been repaired once this process.
|
|
4317
4511
|
*
|
|
4318
4512
|
* A caller with nothing granted never gains a pointer from a sync, so without
|
|
@@ -4972,7 +5166,6 @@ function apply(ctx, deploymentConfig) {
|
|
|
4972
5166
|
content: encoding === "base64" ? void 0 : content,
|
|
4973
5167
|
...encoding === "base64" ? { bytes: payloadBytes } : {}
|
|
4974
5168
|
});
|
|
4975
|
-
const extraction = await extractBeside(config, relPath, uploadSettings.mineruBaseUrl);
|
|
4976
5169
|
await appendAuditEntry(uploadViewDir, {
|
|
4977
5170
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4978
5171
|
actor: caller.userId,
|
|
@@ -4984,7 +5177,7 @@ function apply(ctx, deploymentConfig) {
|
|
|
4984
5177
|
const afterWrite = await refreshViewIfStale(true, created.relPath);
|
|
4985
5178
|
const reply = writeReplyAfterSync({
|
|
4986
5179
|
created,
|
|
4987
|
-
|
|
5180
|
+
extraction: extractionQueue.enqueue(relPath, uploadSettings.mineruBaseUrl)
|
|
4988
5181
|
}, afterWrite);
|
|
4989
5182
|
return writeJson(res, reply.status, reply.body);
|
|
4990
5183
|
} catch (error) {
|
|
@@ -5034,11 +5227,91 @@ function apply(ctx, deploymentConfig) {
|
|
|
5034
5227
|
error: "the permission service did not identify this caller",
|
|
5035
5228
|
detail: callerFailure
|
|
5036
5229
|
});
|
|
5230
|
+
const visible = await listDirectoryWithPermissions(host, config, root, target, callerId);
|
|
5231
|
+
const present = new Set(visible.map((entry) => entry.name));
|
|
5232
|
+
const entries = visible.map((entry) => {
|
|
5233
|
+
if (entry.isDir) return entry;
|
|
5234
|
+
const extractable = isExtractable(entry.relPath);
|
|
5235
|
+
return {
|
|
5236
|
+
...entry,
|
|
5237
|
+
canExtract: extractable,
|
|
5238
|
+
hasMarkdown: extractable ? present.has(entry.name + ".md") : false
|
|
5239
|
+
};
|
|
5240
|
+
});
|
|
5037
5241
|
return writeJson(res, 200, {
|
|
5038
|
-
entries
|
|
5242
|
+
entries,
|
|
5243
|
+
total: entries.length,
|
|
5039
5244
|
permissionsResolved: true
|
|
5040
5245
|
});
|
|
5041
5246
|
}
|
|
5247
|
+
case "extract.status": return writeJson(res, 200, extractionQueue.snapshot());
|
|
5248
|
+
case "extract.retry": {
|
|
5249
|
+
if (config.baseUrl.trim() === "") return writeJson(res, 409, { error: "DataOps permission API is not configured" });
|
|
5250
|
+
const body = await readJsonBody(req);
|
|
5251
|
+
const relPath = String(body.relPath ?? "");
|
|
5252
|
+
if (relPath.trim() === "") return writeJson(res, 400, { error: "relPath is required" });
|
|
5253
|
+
if (!isContainedRelPath(relPath)) return writeJson(res, 400, {
|
|
5254
|
+
error: "path escapes the knowledge root",
|
|
5255
|
+
refused: [relPath]
|
|
5256
|
+
});
|
|
5257
|
+
if (!isExtractable(relPath)) return writeJson(res, 200, {
|
|
5258
|
+
outcome: "unsupported",
|
|
5259
|
+
relPath,
|
|
5260
|
+
message: "this file type has no extractor"
|
|
5261
|
+
});
|
|
5262
|
+
const localRoot = localConfig(config);
|
|
5263
|
+
const target = join(localRoot.actualRoot, relPath);
|
|
5264
|
+
const source = await stat(target).catch(() => void 0);
|
|
5265
|
+
if (source === void 0 || !source.isFile()) return writeJson(res, 404, { error: "no such file" });
|
|
5266
|
+
if (!await canWritePath(host, config, relPath)) return writeJson(res, 403, {
|
|
5267
|
+
error: "write access is required to extract here",
|
|
5268
|
+
refused: [relPath]
|
|
5269
|
+
});
|
|
5270
|
+
const sibling = markdownSiblingOf(relPath);
|
|
5271
|
+
const existing = await stat(join(localRoot.actualRoot, sibling)).catch(() => void 0);
|
|
5272
|
+
if (existing !== void 0 && existing.isFile()) return writeJson(res, 200, {
|
|
5273
|
+
outcome: "exists",
|
|
5274
|
+
relPath,
|
|
5275
|
+
markdown: sibling
|
|
5276
|
+
});
|
|
5277
|
+
const settings = await readSettings(viewPathFor({
|
|
5278
|
+
actualRoot: localRoot.actualRoot,
|
|
5279
|
+
viewRoot: localRoot.viewRoot,
|
|
5280
|
+
userId: (await fetchCaller(host, config).catch(() => void 0))?.userId ?? ""
|
|
5281
|
+
}));
|
|
5282
|
+
const accepted = extractionQueue.enqueue(relPath, settings.mineruBaseUrl);
|
|
5283
|
+
if (body.wait === false) return writeJson(res, 202, {
|
|
5284
|
+
outcome: "queued",
|
|
5285
|
+
relPath,
|
|
5286
|
+
extraction: accepted
|
|
5287
|
+
});
|
|
5288
|
+
const settled = await extractionQueue.settled(relPath);
|
|
5289
|
+
const outcome = settled === void 0 ? {
|
|
5290
|
+
state: "failed",
|
|
5291
|
+
reason: "the extraction task was not recorded"
|
|
5292
|
+
} : {
|
|
5293
|
+
state: settled.state === "succeeded" ? "succeeded" : settled.state === "unsupported" ? "unsupported" : "failed",
|
|
5294
|
+
...settled.markdown === void 0 ? {} : { markdown: settled.markdown },
|
|
5295
|
+
...settled.via === void 0 ? {} : { via: settled.via },
|
|
5296
|
+
...settled.reason === void 0 ? {} : { reason: settled.reason }
|
|
5297
|
+
};
|
|
5298
|
+
if (outcome.state === "succeeded") {
|
|
5299
|
+
const afterWrite = await refreshViewIfStale(true, sibling);
|
|
5300
|
+
const reply = writeReplyAfterSync({
|
|
5301
|
+
outcome: "extracted",
|
|
5302
|
+
relPath,
|
|
5303
|
+
markdown: outcome.markdown,
|
|
5304
|
+
...outcome.via === void 0 ? {} : { via: outcome.via }
|
|
5305
|
+
}, afterWrite);
|
|
5306
|
+
return writeJson(res, reply.status, reply.body);
|
|
5307
|
+
}
|
|
5308
|
+
return writeJson(res, 200, {
|
|
5309
|
+
outcome: outcome.state === "unsupported" ? "unsupported" : "failed",
|
|
5310
|
+
relPath,
|
|
5311
|
+
...outcome.reason === void 0 ? {} : { reason: outcome.reason },
|
|
5312
|
+
...outcome.markdown === void 0 ? {} : { markdown: outcome.markdown }
|
|
5313
|
+
});
|
|
5314
|
+
}
|
|
5042
5315
|
case "search": {
|
|
5043
5316
|
const body = await readJsonBody(req);
|
|
5044
5317
|
const term = String(body.term ?? "");
|
|
@@ -5172,13 +5445,17 @@ function apply(ctx, deploymentConfig) {
|
|
|
5172
5445
|
const plan = localConfig(config);
|
|
5173
5446
|
const caller = await fetchCaller(host, config);
|
|
5174
5447
|
if (!isSafeUserId(caller.userId)) return writeJson(res, 400, { error: "unsafe caller id; cannot key a view by it" });
|
|
5448
|
+
const settings = await readSettings(viewPathFor({
|
|
5449
|
+
actualRoot: plan.actualRoot,
|
|
5450
|
+
viewRoot: plan.viewRoot,
|
|
5451
|
+
userId: caller.userId
|
|
5452
|
+
}));
|
|
5453
|
+
const canEdit = await canManageGrants(host, config);
|
|
5454
|
+
const mineruUrl = mineruUrlFor(config);
|
|
5175
5455
|
return writeJson(res, 200, {
|
|
5176
|
-
settings
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
userId: caller.userId
|
|
5180
|
-
})),
|
|
5181
|
-
canEdit: await canManageGrants(host, config)
|
|
5456
|
+
settings,
|
|
5457
|
+
canEdit,
|
|
5458
|
+
...mineruUrl === void 0 ? {} : { mineruUrl }
|
|
5182
5459
|
});
|
|
5183
5460
|
}
|
|
5184
5461
|
case "settings.set": {
|
|
@@ -5280,7 +5557,11 @@ function apply(ctx, deploymentConfig) {
|
|
|
5280
5557
|
outcome: "applied"
|
|
5281
5558
|
}).catch(() => void 0);
|
|
5282
5559
|
const afterWrite = await refreshViewIfStale(true, created.relPath);
|
|
5283
|
-
const
|
|
5560
|
+
const approveSettings = await readSettings(viewDir);
|
|
5561
|
+
const reply = writeReplyAfterSync({
|
|
5562
|
+
approved: created,
|
|
5563
|
+
extraction: extractionQueue.enqueue(created.relPath, approveSettings.mineruBaseUrl)
|
|
5564
|
+
}, afterWrite);
|
|
5284
5565
|
return writeJson(res, reply.status, reply.body);
|
|
5285
5566
|
} catch (error) {
|
|
5286
5567
|
if (error instanceof KnowledgeWriteError) return writeJson(res, writeStatusFor(error.code), {
|
|
@@ -5721,4 +6002,4 @@ async function executeStep(runner, step, hostId, record) {
|
|
|
5721
6002
|
}
|
|
5722
6003
|
}
|
|
5723
6004
|
//#endregion
|
|
5724
|
-
export { Config, KB_ROOT_RESOURCE_ID, KNOWLEDGE_SKILL_DESCRIPTION, KNOWLEDGE_SKILL_INVOCATION, KNOWLEDGE_SKILL_NAME, KNOWLEDGE_SKILL_PROVIDER, KNOWLEDGE_SKILL_RANK, KnowledgeError, KnowledgeWriteError, MAX_KNOWLEDGE_WRITE_BYTES, TEMPLATE_READ_TIMEOUT_MS, apply, canManageGrants, canReadPath, canWritePath, classifyMount, createGrant, createKnowledgeEntry, extractBeside, fetchCaller, fetchGrants, fetchPrincipals, inject, knowledgeAncestorsFor, knowledgeBaseSkillProvider, knowledgeSkillPath, listDirectory, listDirectoryShapes, listDirectoryWithPermissions, listRelativeFiles, localConfig, localStatus, mineruUrlFor, name, planContext, readDepartmentOf, readKnowledgeFile, readKnowledgeFileForDownload, revokeGrant, searchKnowledge, syncCallerView, withStoredStorage };
|
|
6005
|
+
export { Config, KB_ROOT_RESOURCE_ID, KNOWLEDGE_SKILL_DESCRIPTION, KNOWLEDGE_SKILL_INVOCATION, KNOWLEDGE_SKILL_NAME, KNOWLEDGE_SKILL_PROVIDER, KNOWLEDGE_SKILL_RANK, KnowledgeError, KnowledgeWriteError, MAX_KNOWLEDGE_WRITE_BYTES, TEMPLATE_READ_TIMEOUT_MS, apply, canManageGrants, canReadPath, canWritePath, classifyMount, createGrant, createKnowledgeEntry, extractBeside, extractionOutcomeOf, fetchCaller, fetchGrants, fetchPrincipals, inject, knowledgeAncestorsFor, knowledgeBaseSkillProvider, knowledgeSkillPath, listDirectory, listDirectoryShapes, listDirectoryWithPermissions, listRelativeFiles, localConfig, localStatus, mineruUrlFor, name, planContext, readDepartmentOf, readKnowledgeFile, readKnowledgeFileForDownload, revokeGrant, searchKnowledge, syncCallerView, withStoredStorage };
|
package/lib/types/api.d.ts
CHANGED
|
@@ -108,6 +108,16 @@ export declare const KB_API: {
|
|
|
108
108
|
readonly templateSave: "template.save";
|
|
109
109
|
/** Remove one template: its document and its metadata record. */
|
|
110
110
|
readonly templateRemove: "template.remove";
|
|
111
|
+
/**
|
|
112
|
+
* Read what this process knows about extraction.
|
|
113
|
+
*
|
|
114
|
+
* The queue lives in memory, so this reports the tasks of the running process only;
|
|
115
|
+
* a file that is neither queued nor beside a `.md` sibling reads as never attempted,
|
|
116
|
+
* which is a fact about this process rather than a claim about the document.
|
|
117
|
+
*/
|
|
118
|
+
readonly extractStatus: "extract.status";
|
|
119
|
+
/** Re-run extraction for one document that could not finish. */
|
|
120
|
+
readonly extractRetry: "extract.retry";
|
|
111
121
|
/** Read the caller's pinned paths. */
|
|
112
122
|
readonly favoritesGet: "favorites.get";
|
|
113
123
|
readonly favoritesSet: "favorites.set";
|
|
@@ -572,3 +582,39 @@ export interface KnowledgeTemplateSaveInput {
|
|
|
572
582
|
/** Set when editing an existing template, so its document is replaced in place. */
|
|
573
583
|
id?: string;
|
|
574
584
|
}
|
|
585
|
+
/** Where one extraction task is in its lifecycle, as the Host reports it. */
|
|
586
|
+
export type ExtractionStateView = 'pending' | 'running' | 'succeeded' | 'failed' | 'unsupported';
|
|
587
|
+
/**
|
|
588
|
+
* One extraction task.
|
|
589
|
+
*
|
|
590
|
+
* Mirrors the Host's own record: the source path, the state, and whatever it managed to
|
|
591
|
+
* learn about the outcome. `reason` is the extractor's verbatim text, which is the only
|
|
592
|
+
* thing that tells a reader whether to retry or give up.
|
|
593
|
+
*/
|
|
594
|
+
export interface ExtractionTaskView {
|
|
595
|
+
/** Knowledge-relative path of the source document. */
|
|
596
|
+
relPath: string;
|
|
597
|
+
/** Where the task is in its lifecycle. */
|
|
598
|
+
state: ExtractionStateView;
|
|
599
|
+
/** When the task was accepted, ISO-8601. */
|
|
600
|
+
queuedAt: string;
|
|
601
|
+
/** When the run started, ISO-8601. */
|
|
602
|
+
startedAt?: string;
|
|
603
|
+
/** When the run reached a terminal state, ISO-8601. */
|
|
604
|
+
finishedAt?: string;
|
|
605
|
+
/** The Markdown sibling that was written, when one was. */
|
|
606
|
+
markdown?: string;
|
|
607
|
+
/** Which authority produced the text. */
|
|
608
|
+
via?: string;
|
|
609
|
+
/** Why the extraction produced nothing. */
|
|
610
|
+
reason?: string;
|
|
611
|
+
}
|
|
612
|
+
/** The extraction queue's snapshot. */
|
|
613
|
+
export interface ExtractionStatusView {
|
|
614
|
+
/** The path of the task running right now, when one is. */
|
|
615
|
+
running?: string;
|
|
616
|
+
/** How many tasks are queued or running. */
|
|
617
|
+
size: number;
|
|
618
|
+
/** Every task this process knows about. */
|
|
619
|
+
tasks: ExtractionTaskView[];
|
|
620
|
+
}
|
|
@@ -34,10 +34,18 @@ export interface ContextTarget {
|
|
|
34
34
|
}
|
|
35
35
|
/** The clipboard the copy/cut entries fill. */
|
|
36
36
|
export interface TreeClipboard {
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Every entry taken, in the order they were selected.
|
|
39
|
+
*
|
|
40
|
+
* A list rather than one path because a multi-row copy is a normal gesture and the
|
|
41
|
+
* paste has to put all of them somewhere. A single-row copy is a one-element list,
|
|
42
|
+
* which is why there is no separate single-row clipboard: one shape means the paste
|
|
43
|
+
* path cannot be right for one row and wrong for five.
|
|
44
|
+
*/
|
|
45
|
+
entries: readonly {
|
|
46
|
+
relPath: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}[];
|
|
41
49
|
/** True when the source is removed on paste (cut); false for a copy request. */
|
|
42
50
|
cut: boolean;
|
|
43
51
|
}
|
|
@@ -45,6 +53,18 @@ export interface TreeClipboard {
|
|
|
45
53
|
export interface KnowledgeContextMenuProps {
|
|
46
54
|
/** The row the menu is open on, or null when closed. */
|
|
47
55
|
target: ContextTarget | null;
|
|
56
|
+
/**
|
|
57
|
+
* Every row the menu acts on, when the reader right-clicked inside a multi-selection.
|
|
58
|
+
*
|
|
59
|
+
* Absent or shorter than two means single-row behaviour, which is why the component
|
|
60
|
+
* stays one menu rather than gaining a batch twin: the entries, the handlers and the
|
|
61
|
+
* clipboard are identical, and only the LABELS change to say how many rows they will
|
|
62
|
+
* touch. A second menu is how the two surfaces drifted apart before.
|
|
63
|
+
*
|
|
64
|
+
* `target` is still the row under the pointer -- it anchors the menu and supplies the
|
|
65
|
+
* name used by rename/download, which have no batch meaning.
|
|
66
|
+
*/
|
|
67
|
+
selection?: readonly ContextTarget[];
|
|
48
68
|
/** Where the pointer was, in viewport coordinates. */
|
|
49
69
|
at: {
|
|
50
70
|
x: number;
|
|
@@ -66,8 +86,13 @@ export interface KnowledgeContextMenuProps {
|
|
|
66
86
|
onFavorite: (path: string, favorite: boolean) => void;
|
|
67
87
|
/** Whether this path is currently favourited. */
|
|
68
88
|
favorite: boolean;
|
|
69
|
-
/**
|
|
70
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Insert references into a conversation, via the picker.
|
|
91
|
+
*
|
|
92
|
+
* `targets` is present only for a multi-row selection; the single-row case passes the
|
|
93
|
+
* one path. The picker renders one dialog either way.
|
|
94
|
+
*/
|
|
95
|
+
onReference: (path: string, isDir: boolean, targets?: readonly ContextTarget[]) => void;
|
|
71
96
|
/** Called after a rename succeeds, so the page reloads the tree. */
|
|
72
97
|
onRenamed: () => void;
|
|
73
98
|
/** Called after a delete-to-trash succeeds, so the page reloads the tree. */
|
|
@@ -79,6 +79,16 @@ export interface KnowledgeFileTreeProps {
|
|
|
79
79
|
* worse than the code they can at least match against their own department.
|
|
80
80
|
*/
|
|
81
81
|
departmentPath?: DepartmentPath;
|
|
82
|
+
/**
|
|
83
|
+
* Whether one path is currently favourited, so its row can draw the star.
|
|
84
|
+
*
|
|
85
|
+
* The borrowed explorer has no row-decoration hook, so the star is applied by this
|
|
86
|
+
* wrapper observing the rows it renders (see `useRowFavorites`). Absent means the
|
|
87
|
+
* caller has no favourites store, which keeps the wrapper from touching the DOM at all.
|
|
88
|
+
*/
|
|
89
|
+
isFavorite?: (relPath: string) => boolean;
|
|
90
|
+
/** The favourites star's accessible name. */
|
|
91
|
+
favoriteMarkLabel?: string;
|
|
82
92
|
}
|
|
83
93
|
/**
|
|
84
94
|
* The knowledge tree, rendered by the product's file explorer.
|
|
@@ -70,6 +70,29 @@ export interface KnowledgeHomeProps {
|
|
|
70
70
|
* re-read; the page owns the tick that triggers it.
|
|
71
71
|
*/
|
|
72
72
|
onTreeChanged?: () => void;
|
|
73
|
+
/**
|
|
74
|
+
* Upload files dropped onto the table area.
|
|
75
|
+
*
|
|
76
|
+
* The table lists documents only -- there is no directory row here to aim at -- so
|
|
77
|
+
* the whole surface is one target and the destination has to be decided rather than
|
|
78
|
+
* read off the row under the pointer. `dropDirectory` carries that decision.
|
|
79
|
+
* Omitted by a read-only caller, which keeps the surface from accepting a drop that
|
|
80
|
+
* would then be refused on the Host.
|
|
81
|
+
* @param directory - the tree-relative directory the files land in.
|
|
82
|
+
* @param files - the dropped files with paths relative to that directory.
|
|
83
|
+
*/
|
|
84
|
+
onUpload?: (directory: string, files: readonly {
|
|
85
|
+
file: unknown;
|
|
86
|
+
relativePath: string;
|
|
87
|
+
}[]) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Where a drop on this table lands, tree-relative.
|
|
90
|
+
*
|
|
91
|
+
* The reader's own folder is the right default: `global` is the bucket everyone
|
|
92
|
+
* reads, so defaulting there would publish a private draft without asking. Empty
|
|
93
|
+
* means the mount root, which is what a caller with no personal bucket gets.
|
|
94
|
+
*/
|
|
95
|
+
dropDirectory?: string;
|
|
73
96
|
}
|
|
74
97
|
/**
|
|
75
98
|
* Render the Home surface.
|
|
@@ -73,6 +73,20 @@ export interface KnowledgeListingProps {
|
|
|
73
73
|
* row is the tree's root, which has no parent to paste beside.
|
|
74
74
|
*/
|
|
75
75
|
pasteTargetFor?: (relPath: string, isDir: boolean) => string | null;
|
|
76
|
+
/**
|
|
77
|
+
* Upload external files dropped onto a directory row.
|
|
78
|
+
*
|
|
79
|
+
* Distinct from the row-to-row move handled above: a file dragged in from the
|
|
80
|
+
* desktop carries no `KB_DRAG_MIME`, so it is the only case this callback serves.
|
|
81
|
+
* Omitted by a read-only caller, which keeps the rows from accepting a drop that
|
|
82
|
+
* would then be refused on the Host.
|
|
83
|
+
* @param relPath - the directory the files were dropped onto.
|
|
84
|
+
* @param files - the dropped files with paths relative to that directory.
|
|
85
|
+
*/
|
|
86
|
+
onUpload?: (relPath: string, files: readonly {
|
|
87
|
+
file: unknown;
|
|
88
|
+
relativePath: string;
|
|
89
|
+
}[]) => void;
|
|
76
90
|
/**
|
|
77
91
|
* Called after a menu-driven delete or paste succeeded.
|
|
78
92
|
*
|
|
@@ -21,12 +21,37 @@
|
|
|
21
21
|
import type { GrantView, KnowledgeGrantSubjectType } from '../api.ts';
|
|
22
22
|
import { type RoleSummary } from './roleSummary.ts';
|
|
23
23
|
/** Which reading of the grant table the tabs are showing. */
|
|
24
|
-
export type OverviewTab = 'person' | 'resource';
|
|
24
|
+
export type OverviewTab = 'person' | 'role' | 'resource';
|
|
25
25
|
/** Copy for the overview, supplied by the caller's locale record. */
|
|
26
26
|
export interface OverviewCopy {
|
|
27
27
|
tabByPerson: string;
|
|
28
|
+
tabByRole: string;
|
|
28
29
|
tabByResource: string;
|
|
29
30
|
empty: string;
|
|
31
|
+
/** Empty state per view, so "nobody is directly granted" is not shown for a role. */
|
|
32
|
+
emptyByPerson: string;
|
|
33
|
+
emptyByRole: string;
|
|
34
|
+
emptyByResource: string;
|
|
35
|
+
/** Where a principal's access comes from. */
|
|
36
|
+
sourceDirect: string;
|
|
37
|
+
/** Format "inherited via role X": `{role}` is replaced by the role code. */
|
|
38
|
+
sourceViaRole: (role: string) => string;
|
|
39
|
+
/** Shown when the user directory could not be read, so inheritance is unknowable. */
|
|
40
|
+
sourceUnavailable: string;
|
|
41
|
+
/** Word for a permission set that could not be fully determined. */
|
|
42
|
+
actionsUnknown: string;
|
|
43
|
+
actionsUnknownHint: string;
|
|
44
|
+
/** Column headers unique to the three views. */
|
|
45
|
+
colAccount: string;
|
|
46
|
+
colSource: string;
|
|
47
|
+
/** "{count} member(s)". */
|
|
48
|
+
roleMemberCount: (count: number) => string;
|
|
49
|
+
/** Pager copy, shared with the audit table's wording. */
|
|
50
|
+
pagerPrev: string;
|
|
51
|
+
pagerNext: string;
|
|
52
|
+
pagerStatus: (page: number, pageCount: number, total: number) => string;
|
|
53
|
+
pageSizeLabel: string;
|
|
54
|
+
pageSizeOption: (size: number) => string;
|
|
30
55
|
pathsHeld: (count: number) => string;
|
|
31
56
|
rolesHeld: string;
|
|
32
57
|
actionsHeld: string;
|
|
@@ -75,6 +100,24 @@ export interface KnowledgePermissionOverviewProps {
|
|
|
75
100
|
grants: readonly GrantView[];
|
|
76
101
|
/** Display names by `principalType:principalId`. */
|
|
77
102
|
labels?: Readonly<Record<string, string>>;
|
|
103
|
+
/**
|
|
104
|
+
* The DataOps account directory, when the caller could read it.
|
|
105
|
+
*
|
|
106
|
+
* Absent means the same as an unavailable read: inheritance cannot be attributed, and
|
|
107
|
+
* the page says so instead of listing accounts that look like they hold nothing.
|
|
108
|
+
*/
|
|
109
|
+
directory?: {
|
|
110
|
+
kind: 'available';
|
|
111
|
+
users: ReadonlyArray<{
|
|
112
|
+
id: string;
|
|
113
|
+
label: string;
|
|
114
|
+
username?: string;
|
|
115
|
+
roleCodes?: string[];
|
|
116
|
+
}>;
|
|
117
|
+
} | {
|
|
118
|
+
kind: 'unavailable';
|
|
119
|
+
reason: string;
|
|
120
|
+
};
|
|
78
121
|
/** Built-in role summaries, so the person view can explain what a role means. */
|
|
79
122
|
roles?: readonly RoleSummary[];
|
|
80
123
|
/** Locale copy. */
|
|
@@ -92,8 +135,13 @@ export interface KnowledgePermissionOverviewProps {
|
|
|
92
135
|
busy?: boolean;
|
|
93
136
|
}
|
|
94
137
|
/**
|
|
95
|
-
* Render the permission overview as stat cards over a
|
|
138
|
+
* Render the permission overview as stat cards over a three-tab table.
|
|
139
|
+
*
|
|
140
|
+
* THREE views, not two, because the old pair answered different questions under one
|
|
141
|
+
* heading: "by person" listed ROLE grants, so the only rows were 管理员 and 超级管理员
|
|
142
|
+
* and a reader concluding "these are the people with access" was reading the page
|
|
143
|
+
* correctly and being told something false. A role is not a person.
|
|
96
144
|
*
|
|
97
|
-
* @param props - grants, labels, role summaries,
|
|
145
|
+
* @param props - grants, labels, directory, role summaries, copy and the revoke hook.
|
|
98
146
|
*/
|
|
99
147
|
export declare function KnowledgePermissionOverview(props: KnowledgePermissionOverviewProps): import("react").JSX.Element;
|