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.
Files changed (43) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.en.md +7 -7
  3. package/README.md +8 -8
  4. package/docs/RELEASE-0.5.8.md +14 -0
  5. package/lib/client.js +539 -282
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +483 -23
  8. package/lib/types/client/index.d.ts.map +1 -1
  9. package/lib/types/client/workbench/SessionWriteProgress.d.ts +5 -1
  10. package/lib/types/client/workbench/SessionWriteProgress.d.ts.map +1 -1
  11. package/lib/types/client/workbench/TenderHistoryView.d.ts +10 -0
  12. package/lib/types/client/workbench/TenderHistoryView.d.ts.map +1 -0
  13. package/lib/types/client/workbench/TenderWorkbench.d.ts +5 -1
  14. package/lib/types/client/workbench/TenderWorkbench.d.ts.map +1 -1
  15. package/lib/types/contracts/execution.d.ts +58 -0
  16. package/lib/types/contracts/execution.d.ts.map +1 -0
  17. package/lib/types/contracts/history.d.ts +60 -0
  18. package/lib/types/contracts/history.d.ts.map +1 -0
  19. package/lib/types/contracts/reporting.d.ts +3 -3
  20. package/lib/types/contracts/workflow.d.ts +142 -24
  21. package/lib/types/contracts/workflow.d.ts.map +1 -1
  22. package/lib/types/host/history.d.ts +26 -0
  23. package/lib/types/host/history.d.ts.map +1 -0
  24. package/lib/types/host/http-trust.d.ts +2 -0
  25. package/lib/types/host/http-trust.d.ts.map +1 -1
  26. package/lib/types/host/pipeline/provider-envelope.d.ts +10 -0
  27. package/lib/types/host/pipeline/provider-envelope.d.ts.map +1 -0
  28. package/lib/types/host/pipeline/qcc-adapters.d.ts +1 -1
  29. package/lib/types/host/pipeline/qcc-adapters.d.ts.map +1 -1
  30. package/lib/types/host/projection.d.ts +6 -0
  31. package/lib/types/host/projection.d.ts.map +1 -1
  32. package/lib/types/host/tools/analysis-review-tools.d.ts +213 -36
  33. package/lib/types/host/tools/analysis-review-tools.d.ts.map +1 -1
  34. package/lib/types/host/tools/query-tool.d.ts +71 -12
  35. package/lib/types/host/tools/query-tool.d.ts.map +1 -1
  36. package/lib/types/host/tools/report-tools.d.ts +148 -30
  37. package/lib/types/host/tools/report-tools.d.ts.map +1 -1
  38. package/lib/types/host/tools/rule-tools.d.ts +142 -24
  39. package/lib/types/host/tools/rule-tools.d.ts.map +1 -1
  40. package/lib/types/host/tools/workflow-state-tool.d.ts +5 -5
  41. package/lib/types/index.d.ts.map +1 -1
  42. package/package.json +2 -2
  43. 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 applyTenderProjection(state, event) {
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: 2,
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().finite().nonnegative().optional(),
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) return {
4911
- source,
4912
- status: "failed",
4913
- message: sanitizeMessage(result.error.message || contentText(result))
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
- if (intent.tender !== void 0) executions.push(await executeSource(dependencies, exec, "tender", intent.tender));
5193
- if (intent.proposed !== void 0) executions.push(await executeSource(dependencies, exec, "proposed", intent.proposed));
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: failedState(previous, nextRevision, intentId, message, now),
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;AAmCD,mGAAmG;AACnG,wBAAgB,KAAK,CAAC,GAAG,EAAE,mBAAmB,GAAG,IAAI,CAyIpD"}
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"}