dsh-tender-workbench 0.5.7 → 0.5.8
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/CHANGELOG.md +7 -0
- package/README.en.md +7 -7
- package/README.md +8 -8
- package/docs/RELEASE-0.5.8.md +14 -0
- package/lib/client.js +539 -282
- package/lib/client.js.map +1 -1
- package/lib/index.js +483 -23
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/workbench/SessionWriteProgress.d.ts +5 -1
- package/lib/types/client/workbench/SessionWriteProgress.d.ts.map +1 -1
- package/lib/types/client/workbench/TenderHistoryView.d.ts +10 -0
- package/lib/types/client/workbench/TenderHistoryView.d.ts.map +1 -0
- package/lib/types/client/workbench/TenderWorkbench.d.ts +5 -1
- package/lib/types/client/workbench/TenderWorkbench.d.ts.map +1 -1
- package/lib/types/contracts/execution.d.ts +58 -0
- package/lib/types/contracts/execution.d.ts.map +1 -0
- package/lib/types/contracts/history.d.ts +60 -0
- package/lib/types/contracts/history.d.ts.map +1 -0
- package/lib/types/contracts/reporting.d.ts +3 -3
- package/lib/types/contracts/workflow.d.ts +142 -24
- package/lib/types/contracts/workflow.d.ts.map +1 -1
- package/lib/types/host/history.d.ts +26 -0
- package/lib/types/host/history.d.ts.map +1 -0
- package/lib/types/host/http-trust.d.ts +2 -0
- package/lib/types/host/http-trust.d.ts.map +1 -1
- package/lib/types/host/pipeline/provider-envelope.d.ts +10 -0
- package/lib/types/host/pipeline/provider-envelope.d.ts.map +1 -0
- package/lib/types/host/pipeline/qcc-adapters.d.ts +1 -1
- package/lib/types/host/pipeline/qcc-adapters.d.ts.map +1 -1
- package/lib/types/host/projection.d.ts +6 -0
- package/lib/types/host/projection.d.ts.map +1 -1
- package/lib/types/host/tools/analysis-review-tools.d.ts +213 -36
- package/lib/types/host/tools/analysis-review-tools.d.ts.map +1 -1
- package/lib/types/host/tools/query-tool.d.ts +71 -12
- package/lib/types/host/tools/query-tool.d.ts.map +1 -1
- package/lib/types/host/tools/report-tools.d.ts +148 -30
- package/lib/types/host/tools/report-tools.d.ts.map +1 -1
- package/lib/types/host/tools/rule-tools.d.ts +142 -24
- package/lib/types/host/tools/rule-tools.d.ts.map +1 -1
- package/lib/types/host/tools/workflow-state-tool.d.ts +5 -5
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/docs/RELEASE-0.5.7.md +0 -20
package/lib/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
7
7
|
import ExcelJS from "exceljs";
|
|
8
8
|
import PDFDocument from "pdfkit";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
9
10
|
//#region src/contracts/dataset.ts
|
|
10
11
|
const TENDER_DATA_SOURCES = ["tender", "proposed"];
|
|
11
12
|
const OPPORTUNITY_LIFECYCLES = [
|
|
@@ -217,6 +218,66 @@ const QccProposedSearchArgsSchema = z.object({
|
|
|
217
218
|
function hasSupportedQueryFilter(value) {
|
|
218
219
|
return Object.keys(value).some((key) => key !== "smartSort");
|
|
219
220
|
}
|
|
221
|
+
const ProviderOutcomeSchema = z.enum([
|
|
222
|
+
"data",
|
|
223
|
+
"zero",
|
|
224
|
+
"not-needed",
|
|
225
|
+
"no-permission",
|
|
226
|
+
"failed",
|
|
227
|
+
"unknown"
|
|
228
|
+
]);
|
|
229
|
+
const count = z.number().int().nonnegative();
|
|
230
|
+
const TenderExecutionSchema = z.object({
|
|
231
|
+
operationId: z.string().min(1).max(128),
|
|
232
|
+
status: z.enum([
|
|
233
|
+
"running",
|
|
234
|
+
"succeeded",
|
|
235
|
+
"partial",
|
|
236
|
+
"failed",
|
|
237
|
+
"interrupted"
|
|
238
|
+
]),
|
|
239
|
+
currentAction: z.string().min(1).max(200),
|
|
240
|
+
startedAt: count,
|
|
241
|
+
updatedAt: count,
|
|
242
|
+
finishedAt: count.optional(),
|
|
243
|
+
counts: z.object({
|
|
244
|
+
queried: count,
|
|
245
|
+
succeeded: count,
|
|
246
|
+
zero: count,
|
|
247
|
+
failed: count,
|
|
248
|
+
noPermission: count,
|
|
249
|
+
unknown: count,
|
|
250
|
+
needsReview: count
|
|
251
|
+
}).strict(),
|
|
252
|
+
recentItem: z.string().max(200).optional(),
|
|
253
|
+
queryTarget: z.string().max(2048).optional(),
|
|
254
|
+
providers: z.object({
|
|
255
|
+
tender: ProviderOutcomeSchema,
|
|
256
|
+
proposed: ProviderOutcomeSchema
|
|
257
|
+
}).strict()
|
|
258
|
+
}).strict();
|
|
259
|
+
function emptyExecution(operationId, currentAction, time) {
|
|
260
|
+
return {
|
|
261
|
+
operationId,
|
|
262
|
+
currentAction,
|
|
263
|
+
status: "running",
|
|
264
|
+
startedAt: time,
|
|
265
|
+
updatedAt: time,
|
|
266
|
+
counts: {
|
|
267
|
+
queried: 0,
|
|
268
|
+
succeeded: 0,
|
|
269
|
+
zero: 0,
|
|
270
|
+
failed: 0,
|
|
271
|
+
noPermission: 0,
|
|
272
|
+
unknown: 0,
|
|
273
|
+
needsReview: 0
|
|
274
|
+
},
|
|
275
|
+
providers: {
|
|
276
|
+
tender: "not-needed",
|
|
277
|
+
proposed: "not-needed"
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
220
281
|
//#endregion
|
|
221
282
|
//#region src/contracts/orchestration.ts
|
|
222
283
|
const TENDER_SKILL_CONTRACT_MARKER = "dsh-tender-workbench/workflow-v2";
|
|
@@ -554,6 +615,7 @@ const TenderWorkflowProjectionV2Schema = z.object({
|
|
|
554
615
|
revision: z.number().int().nonnegative(),
|
|
555
616
|
currentStage: z.enum(WORKFLOW_STAGES),
|
|
556
617
|
observedTurn: z.number().int().positive().optional(),
|
|
618
|
+
execution: TenderExecutionSchema.optional(),
|
|
557
619
|
pendingIntent: z.object({
|
|
558
620
|
intentId: idText$5,
|
|
559
621
|
kind: z.enum(TENDER_INTENT_KINDS),
|
|
@@ -593,12 +655,14 @@ const TenderWorkflowProjectionV2Schema = z.object({
|
|
|
593
655
|
tender: z.object({
|
|
594
656
|
status: z.enum(["succeeded", "failed"]),
|
|
595
657
|
loaded: z.number().int().nonnegative(),
|
|
658
|
+
outcome: ProviderOutcomeSchema.optional(),
|
|
596
659
|
errorMessage: errorText.optional(),
|
|
597
660
|
sourceData: ArtifactRefV1Schema.optional()
|
|
598
661
|
}).strict().optional(),
|
|
599
662
|
proposed: z.object({
|
|
600
663
|
status: z.enum(["succeeded", "failed"]),
|
|
601
664
|
loaded: z.number().int().nonnegative(),
|
|
665
|
+
outcome: ProviderOutcomeSchema.optional(),
|
|
602
666
|
errorMessage: errorText.optional(),
|
|
603
667
|
sourceData: ArtifactRefV1Schema.optional()
|
|
604
668
|
}).strict().optional()
|
|
@@ -1407,6 +1471,23 @@ function isLoopbackHost(value) {
|
|
|
1407
1471
|
return false;
|
|
1408
1472
|
}
|
|
1409
1473
|
}
|
|
1474
|
+
/** Profile metadata is same-origin read-only; no artifact capability is returned. */
|
|
1475
|
+
function historyRequestIdentity(request) {
|
|
1476
|
+
if (!isLoopbackAddress(request.socket.remoteAddress)) return void 0;
|
|
1477
|
+
const hosts = rawHeaderValues(request, "host"), sessions = rawHeaderValues(request, "x-dsh-tender-session");
|
|
1478
|
+
const origins = rawHeaderValues(request, "origin"), sites = rawHeaderValues(request, "sec-fetch-site");
|
|
1479
|
+
if (hosts.length !== 1 || sessions.length !== 1 || origins.length > 1 || sites.length > 1) return void 0;
|
|
1480
|
+
if (!isLoopbackHost(hosts[0]) || !sessions[0] || sessions[0].length > 128) return void 0;
|
|
1481
|
+
if (sites[0] !== void 0 && sites[0] !== "same-origin") return void 0;
|
|
1482
|
+
if (origins[0] !== void 0) try {
|
|
1483
|
+
const origin = new URL(origins[0]);
|
|
1484
|
+
if (origin.protocol !== "http:" || origin.host !== hosts[0]) return void 0;
|
|
1485
|
+
} catch {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
else if (sites[0] !== "same-origin") return void 0;
|
|
1489
|
+
return sessions[0];
|
|
1490
|
+
}
|
|
1410
1491
|
function artifactRequestIdentity(request) {
|
|
1411
1492
|
if (!isLoopbackAddress(request.socket.remoteAddress)) return void 0;
|
|
1412
1493
|
const hosts = rawHeaderValues(request, "host");
|
|
@@ -3627,7 +3708,7 @@ function pendingFromConversation(state, tool, intentId) {
|
|
|
3627
3708
|
bindingFingerprint: `conversation_${intentId}`.slice(0, 128)
|
|
3628
3709
|
};
|
|
3629
3710
|
}
|
|
3630
|
-
function
|
|
3711
|
+
function applyWorkflowFacts(state, event) {
|
|
3631
3712
|
if (event.type === "turn/start") return {
|
|
3632
3713
|
...state ?? createEmptyTenderWorkflowProjection(),
|
|
3633
3714
|
observedTurn: event.data.turn
|
|
@@ -3737,9 +3818,61 @@ function applyTenderProjection(state, event) {
|
|
|
3737
3818
|
}
|
|
3738
3819
|
return restored;
|
|
3739
3820
|
}
|
|
3821
|
+
/** Execution telemetry is independent of business stages and has a one-way terminal latch. */
|
|
3822
|
+
function applyTenderProjection(state, event) {
|
|
3823
|
+
if (event.type === "dsh-tender/progress") {
|
|
3824
|
+
const parsed = TenderExecutionSchema.safeParse(event.data);
|
|
3825
|
+
if (!parsed.success || !state?.activeOperation || state.activeOperation.callId !== parsed.data.operationId) return state;
|
|
3826
|
+
if (state.execution?.operationId === parsed.data.operationId && state.execution.status !== "running") return state;
|
|
3827
|
+
if (state.execution?.operationId === parsed.data.operationId && state.execution.updatedAt > parsed.data.updatedAt) return state;
|
|
3828
|
+
return {
|
|
3829
|
+
...state,
|
|
3830
|
+
execution: parsed.data
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
const next = applyWorkflowFacts(state, event);
|
|
3834
|
+
if (next === null) return next;
|
|
3835
|
+
let execution = next.execution ?? state?.execution;
|
|
3836
|
+
if (event.type === "tool/call" && next.activeOperation?.callId === String(event.data.callId) && state?.activeOperation?.callId !== next.activeOperation.callId && state?.execution?.operationId !== next.activeOperation.callId) {
|
|
3837
|
+
execution = emptyExecution(next.activeOperation.callId, next.activeOperation.tool, event.time);
|
|
3838
|
+
if (state?.query) {
|
|
3839
|
+
if (state.execution) {
|
|
3840
|
+
execution.counts = { ...state.execution.counts };
|
|
3841
|
+
execution.providers = { ...state.execution.providers };
|
|
3842
|
+
}
|
|
3843
|
+
execution.counts.succeeded = state.query.total;
|
|
3844
|
+
execution.counts.queried = Object.keys(state.query.sources).length;
|
|
3845
|
+
execution.counts.needsReview = state.review?.pending ?? state.classification?.manualReview ?? 0;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
if (execution?.status === "running" && (event.type === "tool/result" && String(event.data.message.source.callId) === execution.operationId || event.type === "turn/end" && state?.observedTurn === event.data.turn)) {
|
|
3849
|
+
let reportedFailure = false;
|
|
3850
|
+
if (event.type === "tool/result") try {
|
|
3851
|
+
reportedFailure = parseTenderToolMetaV2(event.data.meta).effect === "failed";
|
|
3852
|
+
} catch {
|
|
3853
|
+
reportedFailure = true;
|
|
3854
|
+
}
|
|
3855
|
+
const failed = event.type === "tool/result" && (event.data.error !== void 0 || event.data.message.content[0].isError === true || reportedFailure || next.lastFailure !== void 0 && JSON.stringify(next.lastFailure) !== JSON.stringify(state?.lastFailure));
|
|
3856
|
+
execution = {
|
|
3857
|
+
...execution,
|
|
3858
|
+
updatedAt: event.time,
|
|
3859
|
+
finishedAt: event.time,
|
|
3860
|
+
status: event.type === "turn/end" ? "interrupted" : failed ? "failed" : execution.counts.failed + execution.counts.noPermission + execution.counts.unknown > 0 ? "partial" : "succeeded",
|
|
3861
|
+
currentAction: event.type === "turn/end" ? "执行中断,请核对来源会话" : failed ? "本次操作失败" : "本次操作已结束",
|
|
3862
|
+
counts: {
|
|
3863
|
+
...execution.counts,
|
|
3864
|
+
needsReview: next.review?.pending ?? next.classification?.manualReview ?? 0
|
|
3865
|
+
}
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
return execution === void 0 || next.execution === execution ? next : {
|
|
3869
|
+
...next,
|
|
3870
|
+
execution
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3740
3873
|
const tenderWorkflowProjectionDefinition = {
|
|
3741
3874
|
key: "dshTenderWorkflow",
|
|
3742
|
-
stateVersion:
|
|
3875
|
+
stateVersion: 3,
|
|
3743
3876
|
stateSchema: TenderWorkflowProjectionV2Schema.nullable(),
|
|
3744
3877
|
init: () => null,
|
|
3745
3878
|
apply: applyTenderProjection,
|
|
@@ -3923,6 +4056,41 @@ function nestedToolExecution(exec, callId, name, argumentsValue) {
|
|
|
3923
4056
|
};
|
|
3924
4057
|
}
|
|
3925
4058
|
//#endregion
|
|
4059
|
+
//#region src/host/pipeline/provider-envelope.ts
|
|
4060
|
+
const object = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4061
|
+
var ProviderEnvelopeError = class extends Error {
|
|
4062
|
+
outcome;
|
|
4063
|
+
constructor(outcome) {
|
|
4064
|
+
super(outcome === "no-permission" ? "来源权限不足,请检查客户 MCP 授权。" : outcome === "failed" ? "来源明确返回失败,请检查连接后重试。" : "来源返回结构未知,未将其视为零记录。");
|
|
4065
|
+
this.outcome = outcome;
|
|
4066
|
+
}
|
|
4067
|
+
};
|
|
4068
|
+
/** Bounded, explicit QCC/MCP wrappers; never search arbitrary nested customer data. */
|
|
4069
|
+
function unwrapProviderEnvelope(raw, source) {
|
|
4070
|
+
let value = raw;
|
|
4071
|
+
const key = source === "tender" ? "标讯列表" : "拟建项目列表";
|
|
4072
|
+
for (let depth = 0; depth < 5; depth++) {
|
|
4073
|
+
if (!object(value)) throw new ProviderEnvelopeError("unknown");
|
|
4074
|
+
const code = value["Status"] ?? value["statusCode"] ?? value["code"];
|
|
4075
|
+
if ([
|
|
4076
|
+
"401",
|
|
4077
|
+
"403",
|
|
4078
|
+
"FORBIDDEN",
|
|
4079
|
+
"UNAUTHORIZED",
|
|
4080
|
+
"NO_PERMISSION"
|
|
4081
|
+
].includes(String(code).toUpperCase())) throw new ProviderEnvelopeError("no-permission");
|
|
4082
|
+
if (value["isError"] === true || value["success"] === false || value["error"] != null || code !== void 0 && !["0", "200"].includes(String(code))) throw new ProviderEnvelopeError("failed");
|
|
4083
|
+
if (Object.hasOwn(value, key)) {
|
|
4084
|
+
if (!Array.isArray(value[key])) throw new ProviderEnvelopeError("unknown");
|
|
4085
|
+
return value;
|
|
4086
|
+
}
|
|
4087
|
+
const next = value["Result"] ?? value["result"] ?? value["data"] ?? value["structuredContent"];
|
|
4088
|
+
if (next === void 0) throw new ProviderEnvelopeError("unknown");
|
|
4089
|
+
value = next;
|
|
4090
|
+
}
|
|
4091
|
+
throw new ProviderEnvelopeError("unknown");
|
|
4092
|
+
}
|
|
4093
|
+
//#endregion
|
|
3926
4094
|
//#region src/host/pipeline/qcc-adapters.ts
|
|
3927
4095
|
const sourceText = z.string().max(32768);
|
|
3928
4096
|
const requiredSourceText = sourceText.refine((value) => value.trim() !== "", "required source identifier is empty");
|
|
@@ -3935,7 +4103,7 @@ const sourceEntitySchema = z.object({
|
|
|
3935
4103
|
const sourceEntitiesSchema = z.preprocess((value) => value === null ? void 0 : value, z.array(sourceEntitySchema).max(500).optional());
|
|
3936
4104
|
const sourceStringsSchema = z.preprocess((value) => value === null ? void 0 : value, z.array(sourceText).max(500).optional());
|
|
3937
4105
|
const querySummarySchema = z.object({
|
|
3938
|
-
命中总数: z.number().
|
|
4106
|
+
命中总数: z.preprocess((value) => typeof value === "string" && /^\d+$/u.test(value) ? Number(value) : value, z.number().int().nonnegative().safe().optional()),
|
|
3939
4107
|
结果说明: optionalSourceText,
|
|
3940
4108
|
生效筛选: z.preprocess((value) => value === null ? void 0 : value, z.record(z.string(), z.unknown()).optional())
|
|
3941
4109
|
}).passthrough();
|
|
@@ -4031,6 +4199,7 @@ function hasEnvelopeMarker(value, listKey) {
|
|
|
4031
4199
|
return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.hasOwn(value, "查询摘要") || Object.hasOwn(value, listKey));
|
|
4032
4200
|
}
|
|
4033
4201
|
function adaptQccTenderPayload(value) {
|
|
4202
|
+
value = unwrapProviderEnvelope(value, "tender");
|
|
4034
4203
|
if (!hasEnvelopeMarker(value, "标讯列表")) throw new QccSourceContractError("invalid-tender-payload");
|
|
4035
4204
|
const envelope = qccTenderPayloadEnvelopeSchema.safeParse(value);
|
|
4036
4205
|
if (!envelope.success) throw new QccSourceContractError("invalid-tender-payload");
|
|
@@ -4041,6 +4210,7 @@ function adaptQccTenderPayload(value) {
|
|
|
4041
4210
|
};
|
|
4042
4211
|
}
|
|
4043
4212
|
function adaptQccProposedPayload(value) {
|
|
4213
|
+
value = unwrapProviderEnvelope(value, "proposed");
|
|
4044
4214
|
if (!hasEnvelopeMarker(value, "拟建项目列表")) throw new QccSourceContractError("invalid-proposed-payload");
|
|
4045
4215
|
const envelope = qccProposedPayloadEnvelopeSchema.safeParse(value);
|
|
4046
4216
|
if (!envelope.success) throw new QccSourceContractError("invalid-proposed-payload");
|
|
@@ -4858,16 +5028,6 @@ function sanitizeMessage(value) {
|
|
|
4858
5028
|
const sanitized = value.replaceAll(/\p{C}/gu, "").trim();
|
|
4859
5029
|
return (sanitized === "" ? "来源工具调用失败。" : sanitized).slice(0, 512);
|
|
4860
5030
|
}
|
|
4861
|
-
function contentText(result) {
|
|
4862
|
-
const texts = [];
|
|
4863
|
-
const visit = (value) => {
|
|
4864
|
-
if (typeof value !== "object" || value === null) return;
|
|
4865
|
-
if ("text" in value && typeof value.text === "string") texts.push(value.text);
|
|
4866
|
-
if ("content" in value && Array.isArray(value.content)) value.content.forEach(visit);
|
|
4867
|
-
};
|
|
4868
|
-
result.content.forEach(visit);
|
|
4869
|
-
return texts.join("\n");
|
|
4870
|
-
}
|
|
4871
5031
|
function jsonValue$5(value) {
|
|
4872
5032
|
const serialized = JSON.stringify(value);
|
|
4873
5033
|
if (serialized === void 0) throw new TypeError("value is not lossless JSON");
|
|
@@ -4882,6 +5042,7 @@ function hasSourceList(value, source) {
|
|
|
4882
5042
|
function extractMcpCanonicalPayloadCandidates(value) {
|
|
4883
5043
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("MCP canonical value must be an object");
|
|
4884
5044
|
const record = value;
|
|
5045
|
+
if (record["isError"] === true) throw new ProviderEnvelopeError("failed");
|
|
4885
5046
|
const candidates = [];
|
|
4886
5047
|
if (record["structuredContent"] !== void 0) candidates.push(jsonValue$5(record["structuredContent"]));
|
|
4887
5048
|
const content = record["content"];
|
|
@@ -4907,13 +5068,22 @@ async function executeSource(dependencies, exec, source, args) {
|
|
|
4907
5068
|
const callId = `${String(exec.callId)}:qcc:${source}`;
|
|
4908
5069
|
const result = await dependencies.tools.execute(nestedToolExecution(exec, callId, name, args));
|
|
4909
5070
|
exec.signal.throwIfAborted();
|
|
4910
|
-
if (result.isError)
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
5071
|
+
if (result.isError) {
|
|
5072
|
+
const code = String(result.error.info?.code ?? "");
|
|
5073
|
+
return {
|
|
5074
|
+
source,
|
|
5075
|
+
status: "failed",
|
|
5076
|
+
outcome: /^(401|403|FORBIDDEN|UNAUTHORIZED|NO_PERMISSION)$/iu.test(code) ? "no-permission" : "failed",
|
|
5077
|
+
message: "来源工具调用失败,请检查连接与授权。"
|
|
5078
|
+
};
|
|
5079
|
+
}
|
|
4915
5080
|
try {
|
|
4916
5081
|
const candidates = [...extractMcpCanonicalPayloadCandidates(result.value)].sort((left, right) => Number(hasSourceList(right, source)) - Number(hasSourceList(left, source)));
|
|
5082
|
+
for (const payload of candidates) try {
|
|
5083
|
+
unwrapProviderEnvelope(payload, source);
|
|
5084
|
+
} catch (error) {
|
|
5085
|
+
if (error instanceof ProviderEnvelopeError && error.outcome !== "unknown") throw error;
|
|
5086
|
+
}
|
|
4917
5087
|
let contractError;
|
|
4918
5088
|
for (const payload of candidates) try {
|
|
4919
5089
|
return {
|
|
@@ -4930,6 +5100,7 @@ async function executeSource(dependencies, exec, source, args) {
|
|
|
4930
5100
|
return {
|
|
4931
5101
|
source,
|
|
4932
5102
|
status: "failed",
|
|
5103
|
+
outcome: error instanceof ProviderEnvelopeError ? error.outcome : "unknown",
|
|
4933
5104
|
message: sanitizeMessage(error instanceof Error ? error.message : "来源结果无法校验。")
|
|
4934
5105
|
};
|
|
4935
5106
|
}
|
|
@@ -4964,10 +5135,12 @@ function succeededState(revision, intent, now, querySpec, normalizedData, source
|
|
|
4964
5135
|
const sourceState = Object.fromEntries(executions.map((execution) => [execution.source, execution.status === "succeeded" ? {
|
|
4965
5136
|
status: "succeeded",
|
|
4966
5137
|
loaded: execution.adapted.rawRecordCount,
|
|
5138
|
+
outcome: execution.adapted.rawRecordCount === 0 ? "zero" : "data",
|
|
4967
5139
|
sourceData: sourceArtifacts[execution.source]
|
|
4968
5140
|
} : {
|
|
4969
5141
|
status: "failed",
|
|
4970
5142
|
loaded: 0,
|
|
5143
|
+
outcome: execution.outcome,
|
|
4971
5144
|
errorMessage: execution.message
|
|
4972
5145
|
}]));
|
|
4973
5146
|
return TenderWorkflowProjectionV2Schema.parse({
|
|
@@ -5189,8 +5362,59 @@ function createTenderWorkbenchQueryTool(dependencies) {
|
|
|
5189
5362
|
revisionOf: (result) => QueryToolResultV2Schema.parse(result).state.revision,
|
|
5190
5363
|
execute: async (nextRevision) => {
|
|
5191
5364
|
const executions = [];
|
|
5192
|
-
|
|
5193
|
-
|
|
5365
|
+
let progress = emptyExecution(String(exec.callId), "准备查询已授权来源", Date.now());
|
|
5366
|
+
progress.queryTarget = intent.target;
|
|
5367
|
+
progress.providers = {
|
|
5368
|
+
tender: intent.tender ? "unknown" : "not-needed",
|
|
5369
|
+
proposed: intent.proposed ? "unknown" : "not-needed"
|
|
5370
|
+
};
|
|
5371
|
+
const publish = () => {
|
|
5372
|
+
exec.agent?.session.append?.("dsh-tender/progress", progress);
|
|
5373
|
+
};
|
|
5374
|
+
publish();
|
|
5375
|
+
for (const source of ["tender", "proposed"]) {
|
|
5376
|
+
const args = intent[source];
|
|
5377
|
+
if (args === void 0) continue;
|
|
5378
|
+
progress = {
|
|
5379
|
+
...progress,
|
|
5380
|
+
currentAction: source === "tender" ? "正在查询招投标来源" : "正在查询拟建项目来源",
|
|
5381
|
+
updatedAt: Date.now()
|
|
5382
|
+
};
|
|
5383
|
+
publish();
|
|
5384
|
+
let execution;
|
|
5385
|
+
try {
|
|
5386
|
+
execution = await executeSource(dependencies, exec, source, args);
|
|
5387
|
+
} catch (error) {
|
|
5388
|
+
exec.signal.throwIfAborted();
|
|
5389
|
+
execution = {
|
|
5390
|
+
source,
|
|
5391
|
+
status: "failed",
|
|
5392
|
+
outcome: "failed",
|
|
5393
|
+
message: "来源调用异常,请检查连接后重试。"
|
|
5394
|
+
};
|
|
5395
|
+
}
|
|
5396
|
+
executions.push(execution);
|
|
5397
|
+
const outcome = execution.status === "succeeded" ? execution.adapted.rawRecordCount === 0 ? "zero" : "data" : execution.outcome;
|
|
5398
|
+
progress = {
|
|
5399
|
+
...progress,
|
|
5400
|
+
updatedAt: Date.now(),
|
|
5401
|
+
recentItem: source === "tender" ? "招投标来源已返回" : "拟建项目来源已返回",
|
|
5402
|
+
providers: {
|
|
5403
|
+
...progress.providers,
|
|
5404
|
+
[source]: outcome
|
|
5405
|
+
},
|
|
5406
|
+
counts: {
|
|
5407
|
+
...progress.counts,
|
|
5408
|
+
queried: progress.counts.queried + 1,
|
|
5409
|
+
succeeded: progress.counts.succeeded + (execution.status === "succeeded" ? execution.adapted.items.length : 0),
|
|
5410
|
+
zero: progress.counts.zero + Number(outcome === "zero"),
|
|
5411
|
+
failed: progress.counts.failed + Number(outcome === "failed"),
|
|
5412
|
+
noPermission: progress.counts.noPermission + Number(outcome === "no-permission"),
|
|
5413
|
+
unknown: progress.counts.unknown + Number(outcome === "unknown")
|
|
5414
|
+
}
|
|
5415
|
+
};
|
|
5416
|
+
publish();
|
|
5417
|
+
}
|
|
5194
5418
|
exec.signal.throwIfAborted();
|
|
5195
5419
|
const successes = executions.filter((execution) => execution.status === "succeeded");
|
|
5196
5420
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -5203,7 +5427,10 @@ function createTenderWorkbenchQueryTool(dependencies) {
|
|
|
5203
5427
|
intentId,
|
|
5204
5428
|
outcome: "failed",
|
|
5205
5429
|
message: `查询失败:${message}`.slice(0, 512),
|
|
5206
|
-
state:
|
|
5430
|
+
state: {
|
|
5431
|
+
...failedState(previous, nextRevision, intentId, message, now),
|
|
5432
|
+
execution: progress
|
|
5433
|
+
},
|
|
5207
5434
|
control: {
|
|
5208
5435
|
status: "failed",
|
|
5209
5436
|
reasonCode: "all-sources-failed",
|
|
@@ -5232,6 +5459,7 @@ function createTenderWorkbenchQueryTool(dependencies) {
|
|
|
5232
5459
|
const querySpec = await transaction.stageJson("query-spec", `query-${intentId}.json`, jsonValue$5(intent));
|
|
5233
5460
|
const normalizedData = await transaction.stageJson("normalized-data", `dataset-${intentId}.json`, jsonValue$5(dataset), dataset.rows.length);
|
|
5234
5461
|
const state = succeededState(nextRevision, intent, now, querySpec, normalizedData, sourceArtifacts, executions, dataset.summary);
|
|
5462
|
+
state.execution = progress;
|
|
5235
5463
|
const outcome = successes.length === executions.length ? "succeeded" : "partial";
|
|
5236
5464
|
const message = outcome === "partial" ? `查询部分完成:已保留 ${successes.length} 个可用来源,失败来源已明确记录。` : `查询完成:已生成包含 ${dataset.rows.length} 个规范化项目的新活动快照。`;
|
|
5237
5465
|
return jsonValue$5(QueryToolResultV2Schema.parse({
|
|
@@ -10424,6 +10652,236 @@ function createTenderWorkbenchWorkflowStateTool(dependencies) {
|
|
|
10424
10652
|
});
|
|
10425
10653
|
}
|
|
10426
10654
|
//#endregion
|
|
10655
|
+
//#region src/contracts/history.ts
|
|
10656
|
+
const HistoryEntrySchema = z.object({
|
|
10657
|
+
taskId: z.string().min(1).max(128),
|
|
10658
|
+
originSessionId: z.string().min(1).max(128),
|
|
10659
|
+
originWorkspaceId: z.string().max(128).nullable(),
|
|
10660
|
+
originWorkspaceTitle: z.string().max(200),
|
|
10661
|
+
title: z.string().max(2048),
|
|
10662
|
+
createdAt: z.string(),
|
|
10663
|
+
updatedAt: z.string(),
|
|
10664
|
+
revision: z.number().int().nonnegative(),
|
|
10665
|
+
status: z.enum([
|
|
10666
|
+
"in-progress",
|
|
10667
|
+
"needs-review",
|
|
10668
|
+
"partial",
|
|
10669
|
+
"completed",
|
|
10670
|
+
"failed"
|
|
10671
|
+
]),
|
|
10672
|
+
records: z.number().int().nonnegative().nullable(),
|
|
10673
|
+
reviewed: z.number().int().nonnegative(),
|
|
10674
|
+
pending: z.number().int().nonnegative(),
|
|
10675
|
+
deliverables: z.array(z.enum(["excel", "pdf"])).max(2)
|
|
10676
|
+
}).strict();
|
|
10677
|
+
z.object({
|
|
10678
|
+
schemaVersion: z.literal(1),
|
|
10679
|
+
scope: z.literal("profile"),
|
|
10680
|
+
entries: z.array(HistoryEntrySchema.extend({ sourceAvailable: z.boolean() })),
|
|
10681
|
+
total: z.number().int().nonnegative(),
|
|
10682
|
+
page: z.number().int().positive(),
|
|
10683
|
+
pageSize: z.number().int().positive()
|
|
10684
|
+
}).strict();
|
|
10685
|
+
//#endregion
|
|
10686
|
+
//#region src/host/history.ts
|
|
10687
|
+
const IndexSchema = z.object({
|
|
10688
|
+
schemaVersion: z.literal(1),
|
|
10689
|
+
entries: z.array(HistoryEntrySchema)
|
|
10690
|
+
}).strict();
|
|
10691
|
+
const HISTORY_ROUTE = "/dsh-tender-workbench/api/v1/history";
|
|
10692
|
+
/** One Profile's derivative metadata only. Never stores raw data, tokens, or writable projections. */
|
|
10693
|
+
var TenderHistoryIndex = class {
|
|
10694
|
+
path;
|
|
10695
|
+
entries = /* @__PURE__ */ new Map();
|
|
10696
|
+
tail = Promise.resolve();
|
|
10697
|
+
ready;
|
|
10698
|
+
closed = false;
|
|
10699
|
+
constructor(path) {
|
|
10700
|
+
this.path = path;
|
|
10701
|
+
this.ready = readFile(path, "utf8").then((raw) => {
|
|
10702
|
+
for (const entry of IndexSchema.parse(JSON.parse(raw)).entries) this.entries.set(this.key(entry), entry);
|
|
10703
|
+
}).catch((error) => {
|
|
10704
|
+
if (error.code !== "ENOENT") throw error;
|
|
10705
|
+
});
|
|
10706
|
+
this.ready.catch(() => {});
|
|
10707
|
+
}
|
|
10708
|
+
key(entry) {
|
|
10709
|
+
return `${entry.originSessionId}:${entry.taskId}`;
|
|
10710
|
+
}
|
|
10711
|
+
async put(entry) {
|
|
10712
|
+
if (this.closed) return;
|
|
10713
|
+
const operation = this.tail.then(async () => {
|
|
10714
|
+
await this.ready;
|
|
10715
|
+
const validated = HistoryEntrySchema.parse(entry), key = this.key(validated), old = this.entries.get(key);
|
|
10716
|
+
if (old && (old.revision > entry.revision || old.status === "completed")) return;
|
|
10717
|
+
const next = old ? {
|
|
10718
|
+
...validated,
|
|
10719
|
+
originWorkspaceId: old.originWorkspaceId,
|
|
10720
|
+
originWorkspaceTitle: old.originWorkspaceTitle
|
|
10721
|
+
} : validated;
|
|
10722
|
+
if (JSON.stringify(old) === JSON.stringify(next)) return;
|
|
10723
|
+
const entries = new Map(this.entries);
|
|
10724
|
+
entries.set(key, next);
|
|
10725
|
+
await mkdir(dirname(this.path), {
|
|
10726
|
+
recursive: true,
|
|
10727
|
+
mode: 448
|
|
10728
|
+
});
|
|
10729
|
+
const temporary = `${this.path}.${randomUUID()}.tmp`;
|
|
10730
|
+
await writeFile(temporary, JSON.stringify({
|
|
10731
|
+
schemaVersion: 1,
|
|
10732
|
+
entries: [...entries.values()]
|
|
10733
|
+
}), {
|
|
10734
|
+
mode: 384,
|
|
10735
|
+
flag: "wx"
|
|
10736
|
+
});
|
|
10737
|
+
await rename(temporary, this.path);
|
|
10738
|
+
this.entries = entries;
|
|
10739
|
+
});
|
|
10740
|
+
this.tail = operation.catch(() => {});
|
|
10741
|
+
return operation;
|
|
10742
|
+
}
|
|
10743
|
+
async list() {
|
|
10744
|
+
await this.ready;
|
|
10745
|
+
await this.tail;
|
|
10746
|
+
return [...this.entries.values()].map((entry) => structuredClone(entry)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || a.taskId.localeCompare(b.taskId));
|
|
10747
|
+
}
|
|
10748
|
+
async close() {
|
|
10749
|
+
this.closed = true;
|
|
10750
|
+
await this.tail;
|
|
10751
|
+
}
|
|
10752
|
+
};
|
|
10753
|
+
function historyEntry(header, state, workspace) {
|
|
10754
|
+
const failedAttempt = state.execution;
|
|
10755
|
+
if (failedAttempt?.queryTarget && ["failed", "interrupted"].includes(failedAttempt.status)) return HistoryEntrySchema.parse({
|
|
10756
|
+
taskId: failedAttempt.operationId,
|
|
10757
|
+
originSessionId: String(header.id),
|
|
10758
|
+
originWorkspaceId: workspace?.id ?? null,
|
|
10759
|
+
originWorkspaceTitle: workspace?.title.slice(0, 200) ?? "原任务未绑定 Workspace",
|
|
10760
|
+
title: failedAttempt.queryTarget,
|
|
10761
|
+
createdAt: new Date(failedAttempt.startedAt).toISOString(),
|
|
10762
|
+
updatedAt: new Date(failedAttempt.finishedAt ?? failedAttempt.updatedAt).toISOString(),
|
|
10763
|
+
revision: state.revision,
|
|
10764
|
+
status: "failed",
|
|
10765
|
+
records: null,
|
|
10766
|
+
reviewed: 0,
|
|
10767
|
+
pending: 0,
|
|
10768
|
+
deliverables: []
|
|
10769
|
+
});
|
|
10770
|
+
const query = state.query;
|
|
10771
|
+
if (!query) return void 0;
|
|
10772
|
+
const report = state.report;
|
|
10773
|
+
const completed = report?.completeness === "complete" && report.excel.status === "succeeded" && report.pdf.status === "succeeded";
|
|
10774
|
+
const times = Object.values(state.stages).flatMap((stage) => stage.updatedAt ? [stage.updatedAt] : []);
|
|
10775
|
+
return HistoryEntrySchema.parse({
|
|
10776
|
+
taskId: query.querySpec.id,
|
|
10777
|
+
originSessionId: String(header.id),
|
|
10778
|
+
originWorkspaceId: workspace?.id ?? null,
|
|
10779
|
+
originWorkspaceTitle: workspace?.title.slice(0, 200) ?? "原任务未绑定 Workspace",
|
|
10780
|
+
title: query.targetSummary,
|
|
10781
|
+
createdAt: query.querySpec.createdAt,
|
|
10782
|
+
updatedAt: times.sort().at(-1) ?? query.querySpec.createdAt,
|
|
10783
|
+
revision: state.revision,
|
|
10784
|
+
status: completed ? "completed" : state.stages[state.currentStage].status === "failed" ? "failed" : report?.finalSnapshot ? "partial" : (state.review?.pending ?? state.classification?.manualReview ?? 0) > 0 ? "needs-review" : "in-progress",
|
|
10785
|
+
records: query.total,
|
|
10786
|
+
reviewed: report?.reviewed ?? (state.review ? state.review.confirmedCandidate + state.review.watch + state.review.exclude : 0),
|
|
10787
|
+
pending: state.review?.pending ?? state.classification?.manualReview ?? 0,
|
|
10788
|
+
deliverables: ["excel", "pdf"].filter((format) => report?.[format].status === "succeeded")
|
|
10789
|
+
});
|
|
10790
|
+
}
|
|
10791
|
+
/** Loader root baseUrl is the public, verified Profile anchor; never default to process cwd/HOME. */
|
|
10792
|
+
function profileHistoryPath(loader) {
|
|
10793
|
+
const baseUrl = loader?.ctx?.baseUrl;
|
|
10794
|
+
if (typeof baseUrl !== "string") return void 0;
|
|
10795
|
+
try {
|
|
10796
|
+
const url = new URL(".", baseUrl);
|
|
10797
|
+
if (url.protocol !== "file:") return void 0;
|
|
10798
|
+
return join(fileURLToPath(url), ".dsh-tender-workbench", "history-v1.json");
|
|
10799
|
+
} catch {
|
|
10800
|
+
return;
|
|
10801
|
+
}
|
|
10802
|
+
}
|
|
10803
|
+
function registerTenderHistory(ctx) {
|
|
10804
|
+
if (ctx.webServer.host !== "127.0.0.1") throw new Error("History requires loopback Host");
|
|
10805
|
+
const path = profileHistoryPath(ctx.get("loader"));
|
|
10806
|
+
const index = path ? new TenderHistoryIndex(path) : void 0;
|
|
10807
|
+
let disposed = false, lastError = false;
|
|
10808
|
+
const capture = (session, raw) => {
|
|
10809
|
+
if (!index || disposed) return;
|
|
10810
|
+
const parsed = TenderWorkflowProjectionV2Schema.safeParse(raw);
|
|
10811
|
+
if (!parsed.success) return;
|
|
10812
|
+
try {
|
|
10813
|
+
const workspace = ctx.workspaceRegistry.list().find((item) => item.sessionIds.includes(session.id));
|
|
10814
|
+
const entry = historyEntry(session.header, parsed.data, workspace);
|
|
10815
|
+
if (entry) index.put(entry).catch(() => {
|
|
10816
|
+
lastError = true;
|
|
10817
|
+
});
|
|
10818
|
+
} catch {
|
|
10819
|
+
lastError = true;
|
|
10820
|
+
}
|
|
10821
|
+
};
|
|
10822
|
+
const unsubscribe = ctx.sessionProjections.onChanged((session, key, value) => {
|
|
10823
|
+
if (key === "dshTenderWorkflow") capture(session, value);
|
|
10824
|
+
});
|
|
10825
|
+
for (const session of ctx.sessions.list()) capture(session, ctx.sessionProjections.stateOf(session, "dshTenderWorkflow"));
|
|
10826
|
+
const created = ctx.on("session/created", (session) => capture(session, ctx.sessionProjections.stateOf(session, "dshTenderWorkflow")));
|
|
10827
|
+
const unregister = ctx.webServer.register({
|
|
10828
|
+
kind: "exact",
|
|
10829
|
+
path: HISTORY_ROUTE,
|
|
10830
|
+
async handler(req, res) {
|
|
10831
|
+
const json = (status, value) => {
|
|
10832
|
+
res.writeHead(status, {
|
|
10833
|
+
"Content-Type": "application/json",
|
|
10834
|
+
"Cache-Control": "private, no-store",
|
|
10835
|
+
"X-Content-Type-Options": "nosniff"
|
|
10836
|
+
});
|
|
10837
|
+
res.end(JSON.stringify(value));
|
|
10838
|
+
};
|
|
10839
|
+
if (req.method !== "GET") {
|
|
10840
|
+
json(405, { error: "只读历史" });
|
|
10841
|
+
return;
|
|
10842
|
+
}
|
|
10843
|
+
const identity = historyRequestIdentity(req);
|
|
10844
|
+
if (!identity || !ctx.sessions.get(identity) && !ctx.workspaceRegistry.list().some((workspace) => workspace.sessionIds.includes(identity))) {
|
|
10845
|
+
json(403, { error: "来源请求不可验证" });
|
|
10846
|
+
return;
|
|
10847
|
+
}
|
|
10848
|
+
if (!index || lastError) {
|
|
10849
|
+
json(503, { error: "当前 Profile 历史不可用,请检查宿主能力或本地存储;来源会话不受影响。" });
|
|
10850
|
+
return;
|
|
10851
|
+
}
|
|
10852
|
+
try {
|
|
10853
|
+
const url = new URL(req.url ?? "/dsh-tender-workbench/api/v1/history", "http://127.0.0.1");
|
|
10854
|
+
const page = Number(url.searchParams.get("page") ?? 1), pageSize = 20;
|
|
10855
|
+
if (!Number.isSafeInteger(page) || page < 1 || page > 1e5) {
|
|
10856
|
+
json(400, { error: "页码无效" });
|
|
10857
|
+
return;
|
|
10858
|
+
}
|
|
10859
|
+
const rows = await index.list(), workspaces = ctx.workspaceRegistry.list();
|
|
10860
|
+
json(200, {
|
|
10861
|
+
schemaVersion: 1,
|
|
10862
|
+
scope: "profile",
|
|
10863
|
+
entries: rows.slice((page - 1) * pageSize, page * pageSize).map((entry) => ({
|
|
10864
|
+
...entry,
|
|
10865
|
+
sourceAvailable: workspaces.some((w) => String(w.id) === entry.originWorkspaceId && w.sessionIds.includes(entry.originSessionId))
|
|
10866
|
+
})),
|
|
10867
|
+
page,
|
|
10868
|
+
pageSize,
|
|
10869
|
+
total: rows.length
|
|
10870
|
+
});
|
|
10871
|
+
} catch {
|
|
10872
|
+
json(503, { error: "历史索引读取失败;没有将错误视为空历史。" });
|
|
10873
|
+
}
|
|
10874
|
+
}
|
|
10875
|
+
});
|
|
10876
|
+
return async () => {
|
|
10877
|
+
disposed = true;
|
|
10878
|
+
unsubscribe();
|
|
10879
|
+
created();
|
|
10880
|
+
unregister();
|
|
10881
|
+
await index?.close();
|
|
10882
|
+
};
|
|
10883
|
+
}
|
|
10884
|
+
//#endregion
|
|
10427
10885
|
//#region src/index.ts
|
|
10428
10886
|
/** Public Host services required by the S2/S3 workflow and Session-private Artifact seam. */
|
|
10429
10887
|
const inject = [
|
|
@@ -10432,7 +10890,8 @@ const inject = [
|
|
|
10432
10890
|
"sessionPersistence",
|
|
10433
10891
|
"webServer",
|
|
10434
10892
|
"sessions",
|
|
10435
|
-
"skills"
|
|
10893
|
+
"skills",
|
|
10894
|
+
"workspaceRegistry"
|
|
10436
10895
|
];
|
|
10437
10896
|
function sessionPersistenceLocator(ctx) {
|
|
10438
10897
|
const candidate = ctx.get("sessionPersistence");
|
|
@@ -10443,6 +10902,7 @@ function sessionPersistenceLocator(ctx) {
|
|
|
10443
10902
|
/** Register the whole-Session Projection, high-level tools, and read-only Artifact route. */
|
|
10444
10903
|
function apply(ctx) {
|
|
10445
10904
|
ctx.sessionProjections.register(tenderWorkflowProjectionDefinition);
|
|
10905
|
+
ctx.effect(() => registerTenderHistory(ctx), "dsh-tender-workbench: Profile history");
|
|
10446
10906
|
const persistence = sessionPersistenceLocator(ctx);
|
|
10447
10907
|
const receipts = new IntentReceiptCoordinator();
|
|
10448
10908
|
ctx.effect(() => ctx.tools.register(createTenderWorkbenchQueryTool({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAoB9D,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAiBrD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,2DAA2D;QAC3D,YAAY,EAAE,SAAS,CAAA;KACxB;CACF;AAID,yFAAyF;AACzF,eAAO,MAAM,MAAM,UAElB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAoB9D,OAAO,EAAU,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAiBrD,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,2DAA2D;QAC3D,YAAY,EAAE,SAAS,CAAA;KACxB;CACF;AAID,yFAAyF;AACzF,eAAO,MAAM,MAAM,UAElB,CAAA;AAsCD,mGAAmG;AACnG,wBAAgB,KAAK,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,CA+IpD"}
|