@deployxai/dxc 0.1.2 → 0.1.4

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 (69) hide show
  1. package/README.md +10 -127
  2. package/dist/chunks/{chunk-UO2FNP6N.js → chunk-GCGIIKQJ.js} +1 -1
  3. package/dist/chunks/chunk-JGUPNE36.js +2077 -0
  4. package/dist/chunks/{knowledge-4ZXXUSDQ.js → knowledge-6LOGJETT.js} +902 -22
  5. package/dist/chunks/{monitor-63LXCSZA.js → monitor-CYJPUX5V.js} +2 -2
  6. package/dist/index.js +575 -63
  7. package/package.json +3 -3
  8. package/skills/dxc-article-outline/references/outline-methods.md +20 -0
  9. package/skills/dxc-article-write/SKILL.md +34 -2
  10. package/skills/dxc-article-write/references/writing-methods.md +20 -0
  11. package/skills/dxc-content-brief/references/brief-method.md +18 -0
  12. package/skills/dxc-content-review/SKILL.md +2 -0
  13. package/skills/dxc-content-review/references/review-checklist.md +17 -0
  14. package/skills/dxc-content-workflow/SKILL.md +42 -8
  15. package/skills/dxc-content-workflow/references/onboarding-questions.md +67 -41
  16. package/skills/dxc-content-workflow/references/stage-contract.md +4 -1
  17. package/skills/dxc-knowledge/SKILL.md +30 -0
  18. package/skills/dxc-knowledge/agents/openai.yaml +6 -0
  19. package/skills/dxc-memory/SKILL.md +28 -0
  20. package/skills/dxc-memory/agents/openai.yaml +6 -0
  21. package/skills/dxc-profile/SKILL.md +32 -0
  22. package/skills/dxc-profile/agents/openai.yaml +6 -0
  23. package/skills/dxc-quote-curator/SKILL.md +35 -0
  24. package/skills/dxc-quote-curator/agents/openai.yaml +6 -0
  25. package/skills/dxc-research/references/research-method.md +21 -0
  26. package/skills/dxc-title-write/SKILL.md +6 -1
  27. package/skills/dxc-title-write/references/title-methods.md +15 -0
  28. package/skills/dxc-visual-plan/SKILL.md +24 -4
  29. package/skills/dxc-visual-plan/references/visual-methods.md +18 -0
  30. package/skills/dxc-wechat-publisher/SKILL.md +1 -0
  31. package/dist/chunks/chunk-ZPBFGBWP.js +0 -16422
  32. package/docs/00-project-context.md +0 -125
  33. package/docs/01-north-star-architecture.md +0 -234
  34. package/docs/02-mvp-technical-design.md +0 -553
  35. package/docs/03-domain-state-api.md +0 -600
  36. package/docs/04-security-and-operations.md +0 -413
  37. package/docs/05-delivery-plan.md +0 -407
  38. package/docs/README.md +0 -44
  39. package/docs/decisions/0001-initial-architecture.md +0 -57
  40. package/docs/decisions/0002-mongodb-environment-boundary.md +0 -42
  41. package/docs/decisions/0003-staged-production-topology.md +0 -33
  42. package/docs/decisions/0004-local-first-agent-research-runtime.md +0 -71
  43. package/docs/decisions/0005-official-skill-orchestration-and-local-content-memory.md +0 -97
  44. package/docs/decisions/0006-separate-wechat-user-login-from-account-authorization.md +0 -87
  45. package/docs/decisions/0007-explicit-personal-wechat-start.md +0 -67
  46. package/docs/decisions/0008-end-to-end-content-workflow-continuity.md +0 -115
  47. package/docs/decisions/0009-privileged-multitenant-draft-scheduling.md +0 -36
  48. package/docs/decisions/0009-versioned-cloud-template-catalog.md +0 -39
  49. package/docs/decisions/0010-cli-runtime-and-default-server.md +0 -31
  50. package/docs/decisions/0011-user-memory-and-explicit-history-library.md +0 -30
  51. package/docs/eight-stage-implementation-audit.md +0 -62
  52. package/docs/first-user-guide.md +0 -191
  53. package/docs/history/content-forge-prd-v0.2-summary.md +0 -81
  54. package/docs/local-development.md +0 -520
  55. package/docs/references/aliyun-oss-production-setup.md +0 -89
  56. package/docs/references/legacy-content-to-wechat-contract.md +0 -223
  57. package/docs/references/renderer-compatibility-report.md +0 -68
  58. package/docs/references/source-inventory.md +0 -179
  59. package/docs/references/wechat-renderer-platform-validation.md +0 -92
  60. package/docs/references/wechat-third-party-platform-setup.md +0 -159
  61. package/docs/references/wechat-website-login-setup.md +0 -137
  62. package/docs/references/wemd-template-attribution.md +0 -25
  63. package/docs/research-monitoring-design.md +0 -235
  64. package/docs/todo-preview-local-first.md +0 -31
  65. package/docs/workbuddy-first-user-runbook.md +0 -244
  66. package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-b-research-analyst/SKILL.md +0 -230
  67. package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-c-outline-architect/SKILL.md +0 -194
  68. package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-d-content-writer/SKILL.md +0 -296
  69. package/docs//345/221/230/345/267/245BCDE/347/232/204skill/employee-e-visual-designer/SKILL.md +0 -268
@@ -9,6 +9,11 @@ import {
9
9
  LocalStateError,
10
10
  contentKnowledgeImportResultSchema,
11
11
  contentKnowledgeModelSchema,
12
+ contentKnowledgeQuoteInitResultSchema,
13
+ contentKnowledgeQuoteListResultSchema,
14
+ contentKnowledgeQuoteMutationResultSchema,
15
+ contentKnowledgeQuoteStatusResultSchema,
16
+ contentKnowledgeQuoteSyncResultSchema,
12
17
  contentKnowledgeRemoveResultSchema,
13
18
  contentKnowledgeSearchModeSchema,
14
19
  contentKnowledgeSearchResultSchema,
@@ -19,13 +24,14 @@ import {
19
24
  readPrivateFile,
20
25
  writePrivateBuffer,
21
26
  writePrivateJson
22
- } from "./chunk-ZPBFGBWP.js";
27
+ } from "./chunk-JGUPNE36.js";
23
28
 
24
29
  // apps/cli/src/knowledge.ts
25
30
  import { createHash as createHash2, randomUUID } from "node:crypto";
26
31
  import { lstat, readdir, readFile, stat } from "node:fs/promises";
27
32
  import path2 from "node:path";
28
33
  import { DatabaseSync } from "node:sqlite";
34
+ import { z } from "zod";
29
35
 
30
36
  // apps/cli/src/knowledge-embedding.ts
31
37
  import { createHash } from "node:crypto";
@@ -326,9 +332,14 @@ var LocalBgeKnowledgeEmbedder = class {
326
332
 
327
333
  // apps/cli/src/knowledge.ts
328
334
  var DATABASE_DISPLAY_PATH = "~/.dxc/content-memory.sqlite";
335
+ var QUOTE_SOURCE_DISPLAY_PATH = "~/Documents/DxC/\u91D1\u53E5\u5E93.md";
336
+ var QUOTE_SOURCE_FILE_NAME = "\u91D1\u53E5\u5E93.md";
337
+ var LEGACY_QUOTE_SOURCE_FILE_NAME = "quotes.json";
329
338
  var MAXIMUM_ARTICLE_BYTES = 5 * 1024 * 1024;
330
339
  var MAXIMUM_CHUNK_CHARACTERS = 420;
331
340
  var TARGET_CHUNK_CHARACTERS = 280;
341
+ var MINIMUM_CONTEXTUAL_CHUNK_CHARACTERS = 80;
342
+ var MINIMUM_INDEXABLE_ARTICLE_CHARACTERS = 20;
332
343
  var MAXIMUM_RESULTS = 20;
333
344
  var SEARCH_CANDIDATE_LIMIT = 80;
334
345
  var RRF_CONSTANT = 60;
@@ -346,9 +357,71 @@ var KnowledgeCliError = class extends Error {
346
357
  }
347
358
  code;
348
359
  };
360
+ var quoteMetadataValueSchema = z.string().trim().min(1).max(300).refine((value) => !/[\r\n]/u.test(value) && value !== "\uFF08\u65E0\uFF09", {
361
+ message: "quote metadata must stay on one line"
362
+ });
363
+ var quoteTagSchema = quoteMetadataValueSchema.max(80).refine((value) => !/[,,]/u.test(value), {
364
+ message: "quote tags must not contain separators"
365
+ });
366
+ var quoteSourceItemSchema = z.object({
367
+ attribution: quoteMetadataValueSchema.nullable().optional(),
368
+ origin: z.enum(["original", "third-party"]),
369
+ quoteId: z.uuid(),
370
+ source: quoteMetadataValueSchema.nullable().optional(),
371
+ status: z.enum(["active", "archived"]),
372
+ tags: z.array(quoteTagSchema).max(20).default([]),
373
+ text: z.string().trim().min(1).max(2e3),
374
+ verbatimUse: z.enum(["original", "needs-verification", "permitted", "do-not-use-verbatim"])
375
+ }).strict().superRefine((quote, context) => {
376
+ if (quote.origin === "third-party" && quote.attribution == null) {
377
+ context.addIssue({
378
+ code: "custom",
379
+ message: "Third-party quotes require attribution",
380
+ path: ["attribution"]
381
+ });
382
+ }
383
+ if (quote.origin === "original" && quote.verbatimUse !== "original") {
384
+ context.addIssue({
385
+ code: "custom",
386
+ message: "Original quotes must use the original verbatim-use value",
387
+ path: ["verbatimUse"]
388
+ });
389
+ }
390
+ });
391
+ var quoteSourceDocumentSchema = z.object({
392
+ quotes: z.array(quoteSourceItemSchema).max(5e3),
393
+ schemaVersion: z.literal("1")
394
+ }).strict().superRefine((document, context) => {
395
+ const ids = /* @__PURE__ */ new Set();
396
+ for (const [index, quote] of document.quotes.entries()) {
397
+ if (ids.has(quote.quoteId)) {
398
+ context.addIssue({
399
+ code: "custom",
400
+ message: "quoteId values must be unique",
401
+ path: ["quotes", index, "quoteId"]
402
+ });
403
+ }
404
+ ids.add(quote.quoteId);
405
+ }
406
+ });
349
407
  function errorCode(error) {
350
408
  return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
351
409
  }
410
+ function skippedArticle(error, inputPath) {
411
+ if (!(error instanceof KnowledgeCliError)) {
412
+ return void 0;
413
+ }
414
+ const code = error.code;
415
+ if (code !== "DXC_KNOWLEDGE_ARTICLE_INVALID" && code !== "DXC_KNOWLEDGE_FILE_TOO_LARGE") {
416
+ return void 0;
417
+ }
418
+ const sourceLabel = path2.basename(inputPath).slice(0, 255).trim();
419
+ return {
420
+ code,
421
+ message: error.message,
422
+ sourceLabel: sourceLabel.length === 0 ? "(unknown)" : sourceLabel
423
+ };
424
+ }
352
425
  function sha2562(value) {
353
426
  return createHash2("sha256").update(value).digest("hex");
354
427
  }
@@ -404,10 +477,29 @@ function splitOversizedText(value) {
404
477
  flush();
405
478
  return parts;
406
479
  }
480
+ function characterCount(value) {
481
+ return [...value].length;
482
+ }
483
+ function combinedHeading(left, right) {
484
+ const headings = [left, right].filter((heading) => heading !== null);
485
+ if (headings.length === 0) {
486
+ return null;
487
+ }
488
+ return [...new Set(headings)].join(" / ").slice(0, 300);
489
+ }
490
+ function combineChunks(left, right) {
491
+ return {
492
+ body: `${left.body}
493
+
494
+ ${right.body}`,
495
+ heading: combinedHeading(left.heading, right.heading)
496
+ };
497
+ }
407
498
  function chunkHistoricalArticle(markdown) {
408
499
  const value = removeFrontmatter(markdown);
409
500
  const lines = value.split("\n");
410
501
  const paragraphs = [];
502
+ let fencedCodeDelimiter = null;
411
503
  let heading = null;
412
504
  let paragraphLines = [];
413
505
  const flushParagraph = () => {
@@ -421,8 +513,22 @@ function chunkHistoricalArticle(markdown) {
421
513
  }
422
514
  };
423
515
  for (const line of lines) {
516
+ const fenceMarker = line.match(/^\s*(`{3,}|~{3,})/u)?.[1];
517
+ if (fenceMarker !== void 0) {
518
+ const delimiter = fenceMarker.startsWith("`") ? "`" : "~";
519
+ if (fencedCodeDelimiter === null) {
520
+ fencedCodeDelimiter = delimiter;
521
+ paragraphLines.push(line);
522
+ continue;
523
+ }
524
+ if (fencedCodeDelimiter === delimiter) {
525
+ fencedCodeDelimiter = null;
526
+ paragraphLines.push(line);
527
+ continue;
528
+ }
529
+ }
424
530
  const headingMatch = line.match(/^\s{0,3}#{1,6}\s+(.+)$/u);
425
- if (headingMatch !== null) {
531
+ if (fencedCodeDelimiter === null && headingMatch !== null) {
426
532
  flushParagraph();
427
533
  heading = plainMarkdown(headingMatch[1] ?? "").slice(0, 300).trim() || null;
428
534
  continue;
@@ -452,7 +558,31 @@ ${paragraph.text}`;
452
558
  });
453
559
  }
454
560
  }
455
- return merged.map((chunk, ordinal) => ({
561
+ const contextualized = [];
562
+ let pending;
563
+ for (const chunk of merged) {
564
+ if (pending === void 0) {
565
+ pending = chunk;
566
+ continue;
567
+ }
568
+ const combined = combineChunks(pending, chunk);
569
+ if (characterCount(pending.body) < MINIMUM_CONTEXTUAL_CHUNK_CHARACTERS && characterCount(combined.body) <= MAXIMUM_CHUNK_CHARACTERS) {
570
+ pending = combined;
571
+ continue;
572
+ }
573
+ contextualized.push(pending);
574
+ pending = chunk;
575
+ }
576
+ if (pending !== void 0) {
577
+ const previous = contextualized.at(-1);
578
+ const combined = previous === void 0 ? void 0 : combineChunks(previous, pending);
579
+ if (previous !== void 0 && combined !== void 0 && characterCount(pending.body) < MINIMUM_CONTEXTUAL_CHUNK_CHARACTERS && characterCount(combined.body) <= MAXIMUM_CHUNK_CHARACTERS) {
580
+ contextualized[contextualized.length - 1] = combined;
581
+ } else {
582
+ contextualized.push(pending);
583
+ }
584
+ }
585
+ return contextualized.map((chunk, ordinal) => ({
456
586
  ...chunk,
457
587
  ordinal,
458
588
  sha256: sha2562(chunk.body)
@@ -496,10 +626,10 @@ async function prepareArticle(currentDirectory, inputPath) {
496
626
  }
497
627
  const sourceLabel = path2.basename(absolutePath).slice(0, 255);
498
628
  const chunks = chunkHistoricalArticle(content);
499
- if (chunks.length === 0) {
629
+ if (chunks.length === 0 || chunks.reduce((total, chunk) => total + characterCount(chunk.body), 0) < MINIMUM_INDEXABLE_ARTICLE_CHARACTERS) {
500
630
  throw new KnowledgeCliError(
501
631
  "DXC_KNOWLEDGE_ARTICLE_INVALID",
502
- "Article has no indexable content"
632
+ "Article has too little indexable content"
503
633
  );
504
634
  }
505
635
  return {
@@ -649,10 +779,54 @@ function chunkRow(value) {
649
779
  title: stringColumn(row, "title")
650
780
  };
651
781
  }
782
+ function quoteRow(value) {
783
+ const row = sqlRow(value);
784
+ const nullableString = (name) => {
785
+ const value2 = row[name];
786
+ if (value2 === null) {
787
+ return null;
788
+ }
789
+ return stringColumn(row, name);
790
+ };
791
+ const parseValue = (schema, value2) => {
792
+ const parsed = schema.safeParse(value2);
793
+ if (!parsed.success) {
794
+ throw new KnowledgeCliError(
795
+ "DXC_KNOWLEDGE_STATE_INVALID",
796
+ "Knowledge quote state is invalid"
797
+ );
798
+ }
799
+ return parsed.data;
800
+ };
801
+ return {
802
+ attribution: nullableString("attribution"),
803
+ contentSha256: stringColumn(row, "content_sha256"),
804
+ embedding: vectorFromColumn(row["embedding"]),
805
+ id: stringColumn(row, "quote_id"),
806
+ origin: parseValue(z.enum(["original", "third-party"]), row["origin"]),
807
+ sourceLabel: nullableString("source_label"),
808
+ status: parseValue(z.enum(["active", "archived"]), row["status"]),
809
+ tags: (() => {
810
+ try {
811
+ return parseValue(z.array(z.string()), JSON.parse(stringColumn(row, "tags_json")));
812
+ } catch {
813
+ throw new KnowledgeCliError(
814
+ "DXC_KNOWLEDGE_STATE_INVALID",
815
+ "Knowledge quote state is invalid"
816
+ );
817
+ }
818
+ })(),
819
+ text: stringColumn(row, "body"),
820
+ verbatimUse: parseValue(
821
+ z.enum(["original", "needs-verification", "permitted", "do-not-use-verbatim"]),
822
+ row["verbatim_use"]
823
+ )
824
+ };
825
+ }
652
826
  function databaseSchema(database) {
653
827
  const versionRow = sqlRow(database.prepare("PRAGMA user_version").get());
654
828
  const version = numberColumn(versionRow, "user_version");
655
- if (version !== 0 && version !== Number(CONTENT_KNOWLEDGE_SCHEMA_VERSION)) {
829
+ if (version < 0 || version > Number(CONTENT_KNOWLEDGE_SCHEMA_VERSION)) {
656
830
  throw new KnowledgeCliError(
657
831
  "DXC_KNOWLEDGE_SCHEMA_UNSUPPORTED",
658
832
  "Knowledge database version is unsupported"
@@ -723,8 +897,53 @@ function databaseSchema(database) {
723
897
  tokenize = 'trigram'
724
898
  );
725
899
 
726
- PRAGMA user_version = 1;
727
900
  `);
901
+ if (version < 2) {
902
+ database.exec(`
903
+ CREATE TABLE IF NOT EXISTS quote_sources (
904
+ source_key TEXT PRIMARY KEY,
905
+ content_sha256 TEXT NOT NULL,
906
+ synced_at TEXT NOT NULL
907
+ );
908
+
909
+ CREATE TABLE IF NOT EXISTS quotes (
910
+ quote_id TEXT PRIMARY KEY,
911
+ status TEXT NOT NULL CHECK (status IN ('active', 'archived')),
912
+ body TEXT NOT NULL,
913
+ tags_json TEXT NOT NULL,
914
+ source_label TEXT,
915
+ origin TEXT NOT NULL CHECK (origin IN ('original', 'third-party')),
916
+ attribution TEXT,
917
+ verbatim_use TEXT NOT NULL CHECK (verbatim_use IN ('original', 'needs-verification', 'permitted', 'do-not-use-verbatim')),
918
+ content_sha256 TEXT NOT NULL,
919
+ imported_at TEXT NOT NULL,
920
+ updated_at TEXT NOT NULL,
921
+ embedding BLOB,
922
+ embedding_model TEXT,
923
+ embedding_revision TEXT,
924
+ embedding_quantization TEXT,
925
+ embedding_dimensions INTEGER,
926
+ CHECK (
927
+ (embedding IS NULL AND embedding_model IS NULL AND embedding_revision IS NULL AND embedding_quantization IS NULL AND embedding_dimensions IS NULL)
928
+ OR
929
+ (embedding IS NOT NULL AND embedding_model IS NOT NULL AND embedding_revision IS NOT NULL AND embedding_quantization IS NOT NULL AND embedding_dimensions IS NOT NULL)
930
+ )
931
+ );
932
+
933
+ CREATE INDEX IF NOT EXISTS quotes_status_idx ON quotes(status);
934
+
935
+ CREATE VIRTUAL TABLE IF NOT EXISTS quote_search USING fts5(
936
+ quote_id UNINDEXED,
937
+ body,
938
+ tags,
939
+ source_label,
940
+ attribution,
941
+ tokenize = 'trigram'
942
+ );
943
+
944
+ PRAGMA user_version = 2;
945
+ `);
946
+ }
728
947
  }
729
948
  async function openDatabase(target) {
730
949
  try {
@@ -761,6 +980,310 @@ async function databaseExists(target) {
761
980
  throw new KnowledgeCliError("DXC_KNOWLEDGE_IO_FAILED", "Unable to inspect knowledge database");
762
981
  }
763
982
  }
983
+ function quoteSourcePath(homeDirectory) {
984
+ return path2.join(homeDirectory, "Documents", "DxC", QUOTE_SOURCE_FILE_NAME);
985
+ }
986
+ function legacyQuoteSourcePath(homeDirectory) {
987
+ return path2.join(homeDirectory, ".dxc", LEGACY_QUOTE_SOURCE_FILE_NAME);
988
+ }
989
+ function quoteContentHash(quote) {
990
+ return sha2562(
991
+ JSON.stringify({
992
+ attribution: quote.attribution ?? null,
993
+ origin: quote.origin,
994
+ quoteId: quote.quoteId,
995
+ source: quote.source ?? null,
996
+ status: quote.status,
997
+ tags: [...quote.tags],
998
+ text: quote.text,
999
+ verbatimUse: quote.verbatimUse
1000
+ })
1001
+ );
1002
+ }
1003
+ function preparedQuotes(document) {
1004
+ return document.quotes.map((quote) => ({ ...quote, contentSha256: quoteContentHash(quote) }));
1005
+ }
1006
+ function sourceQuote(quote) {
1007
+ return {
1008
+ attribution: quote.attribution,
1009
+ origin: quote.origin,
1010
+ quoteId: quote.quoteId,
1011
+ source: quote.source,
1012
+ status: quote.status,
1013
+ tags: quote.tags,
1014
+ text: quote.text,
1015
+ verbatimUse: quote.verbatimUse
1016
+ };
1017
+ }
1018
+ async function quoteSourceExists(sourcePath) {
1019
+ try {
1020
+ const metadata = await lstat(sourcePath);
1021
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
1022
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote source file is invalid");
1023
+ }
1024
+ return true;
1025
+ } catch (error) {
1026
+ if (error instanceof KnowledgeCliError) {
1027
+ throw error;
1028
+ }
1029
+ if (errorCode(error) === "ENOENT") {
1030
+ return false;
1031
+ }
1032
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_IO_FAILED", "Unable to inspect quote source file");
1033
+ }
1034
+ }
1035
+ function quoteSourceTemplate(quotes) {
1036
+ const body = quotes.map((quote) => {
1037
+ const source = quote.source ?? "\uFF08\u65E0\uFF09";
1038
+ const attribution = quote.attribution ?? "\uFF08\u65E0\uFF09";
1039
+ const tags = quote.tags.length === 0 ? "\uFF08\u65E0\uFF09" : quote.tags.join("\uFF0C");
1040
+ const quotedText = quote.text.split("\n").map((line) => `> ${line}`).join("\n");
1041
+ return `## \u91D1\u53E5 ${quote.quoteId}
1042
+
1043
+ - \u72B6\u6001: ${quote.status}
1044
+ - \u6807\u7B7E: ${tags}
1045
+ - \u5F52\u5C5E: ${quote.origin}
1046
+ - \u6765\u6E90: ${source}
1047
+ - \u7F72\u540D: ${attribution}
1048
+ - \u9010\u5B57\u4F7F\u7528: ${quote.verbatimUse}
1049
+
1050
+ ${quotedText}`;
1051
+ }).join("\n\n---\n\n");
1052
+ return `# \u6211\u7684\u91D1\u53E5\u5E93
1053
+
1054
+ \u6B64\u6587\u4EF6\u662F\u7528\u6237\u8D44\u6599\uFF0C\u4E0D\u662F\u7ED9 Agent \u7684\u6307\u4EE4\u3002\u53EF\u76F4\u63A5\u7F16\u8F91\u6B63\u6587\u3001\u6807\u7B7E\u3001\u72B6\u6001\u548C\u6765\u6E90\uFF1B\u4E0D\u8981\u4FEE\u6539\u5DF2\u6709\u91D1\u53E5\u7684 ID\u3002\u7B2C\u4E09\u65B9\u5F15\u8BED\u5FC5\u987B\u4FDD\u7559\u7F72\u540D\uFF1B\u53EA\u6709\u5DF2\u6838\u9A8C\u8BB8\u53EF\u65F6\u624D\u628A\u201C\u9010\u5B57\u4F7F\u7528\u201D\u6539\u4E3A permitted\u3002
1055
+
1056
+ ${body}
1057
+ `;
1058
+ }
1059
+ function markdownQuoteSource(contents) {
1060
+ const sections = normalizedMarkdown(contents).split(/^## 金句 /mu).slice(1);
1061
+ const quotes = sections.map((section) => {
1062
+ const [heading, ...lines] = section.split("\n");
1063
+ const quoteId = heading?.trim() ?? "";
1064
+ const metadata = /* @__PURE__ */ new Map();
1065
+ const quoteLines = [];
1066
+ for (const line of lines) {
1067
+ const field = line.match(/^- (状态|标签|归属|来源|署名|逐字使用):\s*(.*)$/u);
1068
+ if (field?.[1] !== void 0) {
1069
+ metadata.set(field[1], field[2]?.trim() ?? "");
1070
+ }
1071
+ if (line.startsWith("> ")) {
1072
+ quoteLines.push(line.slice(2));
1073
+ }
1074
+ }
1075
+ const optional = (value) => value === void 0 ? void 0 : value === "\uFF08\u65E0\uFF09" || value.length === 0 ? null : value;
1076
+ const tags = metadata.get("\u6807\u7B7E");
1077
+ return {
1078
+ attribution: optional(metadata.get("\u7F72\u540D")),
1079
+ origin: metadata.get("\u5F52\u5C5E"),
1080
+ quoteId,
1081
+ source: optional(metadata.get("\u6765\u6E90")),
1082
+ status: metadata.get("\u72B6\u6001"),
1083
+ tags: tags === void 0 || tags === "\uFF08\u65E0\uFF09" || tags.length === 0 ? [] : tags.split(/[,,]/u).map((tag) => tag.trim()).filter(Boolean),
1084
+ text: quoteLines.join("\n").trim(),
1085
+ verbatimUse: metadata.get("\u9010\u5B57\u4F7F\u7528")
1086
+ };
1087
+ });
1088
+ return { quotes, schemaVersion: "1" };
1089
+ }
1090
+ async function readQuoteSource(sourcePath, allowLegacyPermissions = false) {
1091
+ let contents;
1092
+ try {
1093
+ if (allowLegacyPermissions) {
1094
+ const metadata = await lstat(sourcePath);
1095
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAXIMUM_ARTICLE_BYTES) {
1096
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote source file is invalid");
1097
+ }
1098
+ contents = await readFile(sourcePath, "utf8");
1099
+ } else {
1100
+ contents = await readPrivateFile(sourcePath, MAXIMUM_ARTICLE_BYTES);
1101
+ }
1102
+ } catch {
1103
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_IO_FAILED", "Unable to read quote source file");
1104
+ }
1105
+ let parsed;
1106
+ try {
1107
+ parsed = sourcePath.endsWith(".json") ? JSON.parse(contents) : markdownQuoteSource(contents);
1108
+ } catch {
1109
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote source file is invalid");
1110
+ }
1111
+ const document = quoteSourceDocumentSchema.safeParse(parsed);
1112
+ if (!document.success) {
1113
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote source file is invalid");
1114
+ }
1115
+ return {
1116
+ contentSha256: sha2562(contents),
1117
+ quotes: preparedQuotes(document.data)
1118
+ };
1119
+ }
1120
+ async function writeQuoteSource(sourcePath, quotes) {
1121
+ try {
1122
+ await writePrivateBuffer(sourcePath, Buffer.from(quoteSourceTemplate(quotes), "utf8"));
1123
+ } catch {
1124
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_IO_FAILED", "Unable to write quote source file");
1125
+ }
1126
+ }
1127
+ function quoteRecord(quote) {
1128
+ return {
1129
+ attribution: quote.attribution ?? null,
1130
+ origin: quote.origin,
1131
+ quoteId: quote.quoteId,
1132
+ source: quote.source ?? null,
1133
+ status: quote.status,
1134
+ tags: [...quote.tags],
1135
+ text: quote.text,
1136
+ verbatimUse: quote.verbatimUse
1137
+ };
1138
+ }
1139
+ function quoteSourceSummary(database, sourceInitialized) {
1140
+ const counts = sqlRow(
1141
+ database.prepare(
1142
+ `SELECT
1143
+ COALESCE(SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END), 0) AS active_count,
1144
+ COALESCE(SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END), 0) AS archived_count
1145
+ FROM quotes`
1146
+ ).get()
1147
+ );
1148
+ const source = database.prepare("SELECT content_sha256 FROM quote_sources WHERE source_key = 'default' LIMIT 1").get();
1149
+ return {
1150
+ activeCount: numberColumn(counts, "active_count"),
1151
+ archivedCount: numberColumn(counts, "archived_count"),
1152
+ sourceInitialized,
1153
+ sourcePath: QUOTE_SOURCE_DISPLAY_PATH,
1154
+ sourceSha256: source === void 0 ? null : stringColumn(sqlRow(source), "content_sha256")
1155
+ };
1156
+ }
1157
+ function quoteEmbeddingText(quote) {
1158
+ return [quote.text, ...quote.tags, quote.source ?? "", quote.attribution ?? ""].filter((value) => value.length > 0).join("\n");
1159
+ }
1160
+ async function embedQuotes(embedder, quotes) {
1161
+ const activeQuotes = quotes.filter((quote) => quote.status === "active");
1162
+ if (activeQuotes.length === 0) {
1163
+ return /* @__PURE__ */ new Map();
1164
+ }
1165
+ let vectors;
1166
+ try {
1167
+ vectors = await embedder.embedDocuments(activeQuotes.map(quoteEmbeddingText));
1168
+ } catch {
1169
+ throw new KnowledgeCliError(
1170
+ "DXC_KNOWLEDGE_MODEL_FAILED",
1171
+ "Local semantic model could not index quotes"
1172
+ );
1173
+ }
1174
+ if (vectors.length !== activeQuotes.length || vectors.some((vector) => vector.length !== CONTENT_KNOWLEDGE_EMBEDDING_DIMENSIONS)) {
1175
+ throw new KnowledgeCliError(
1176
+ "DXC_KNOWLEDGE_MODEL_FAILED",
1177
+ "Local semantic model returned invalid quote vectors"
1178
+ );
1179
+ }
1180
+ return new Map(
1181
+ activeQuotes.map((quote, index) => [quote.quoteId, vectors[index]])
1182
+ );
1183
+ }
1184
+ function syncQuotes(database, quotes, sourceSha256, vectors, timestamp, allowUnchanged) {
1185
+ const priorSource = database.prepare("SELECT content_sha256 FROM quote_sources WHERE source_key = 'default' LIMIT 1").get();
1186
+ if (allowUnchanged && priorSource !== void 0 && stringColumn(sqlRow(priorSource), "content_sha256") === sourceSha256) {
1187
+ return "unchanged";
1188
+ }
1189
+ database.exec("BEGIN IMMEDIATE");
1190
+ try {
1191
+ const existingRows = database.prepare("SELECT quote_id, content_sha256, imported_at FROM quotes").all().map((value) => sqlRow(value));
1192
+ const existing = new Map(
1193
+ existingRows.map((row) => [
1194
+ stringColumn(row, "quote_id"),
1195
+ {
1196
+ contentSha256: stringColumn(row, "content_sha256"),
1197
+ importedAt: stringColumn(row, "imported_at")
1198
+ }
1199
+ ])
1200
+ );
1201
+ const sourceIds = new Set(quotes.map((quote) => quote.quoteId));
1202
+ database.prepare("DELETE FROM quote_search").run();
1203
+ const upsert = database.prepare(`
1204
+ INSERT INTO quotes (
1205
+ quote_id, status, body, tags_json, source_label, origin, attribution, verbatim_use,
1206
+ content_sha256, imported_at, updated_at, embedding, embedding_model, embedding_revision,
1207
+ embedding_quantization, embedding_dimensions
1208
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1209
+ ON CONFLICT(quote_id) DO UPDATE SET
1210
+ status = excluded.status, body = excluded.body, tags_json = excluded.tags_json,
1211
+ source_label = excluded.source_label, origin = excluded.origin, attribution = excluded.attribution,
1212
+ verbatim_use = excluded.verbatim_use, content_sha256 = excluded.content_sha256,
1213
+ updated_at = excluded.updated_at, embedding = excluded.embedding,
1214
+ embedding_model = excluded.embedding_model, embedding_revision = excluded.embedding_revision,
1215
+ embedding_quantization = excluded.embedding_quantization, embedding_dimensions = excluded.embedding_dimensions
1216
+ `);
1217
+ const insertSearch = database.prepare(`
1218
+ INSERT INTO quote_search (quote_id, body, tags, source_label, attribution) VALUES (?, ?, ?, ?, ?)
1219
+ `);
1220
+ for (const quote of quotes) {
1221
+ const vector = quote.status === "active" ? vectors?.get(quote.quoteId) : void 0;
1222
+ upsert.run(
1223
+ quote.quoteId,
1224
+ quote.status,
1225
+ quote.text,
1226
+ JSON.stringify(quote.tags),
1227
+ quote.source ?? null,
1228
+ quote.origin,
1229
+ quote.attribution ?? null,
1230
+ quote.verbatimUse,
1231
+ quote.contentSha256,
1232
+ existing.get(quote.quoteId)?.importedAt ?? timestamp,
1233
+ timestamp,
1234
+ vector === void 0 ? null : vectorBuffer(vector),
1235
+ vector === void 0 ? null : CONTENT_KNOWLEDGE_EMBEDDING_MODEL,
1236
+ vector === void 0 ? null : CONTENT_KNOWLEDGE_EMBEDDING_REVISION,
1237
+ vector === void 0 ? null : CONTENT_KNOWLEDGE_EMBEDDING_QUANTIZATION,
1238
+ vector === void 0 ? null : CONTENT_KNOWLEDGE_EMBEDDING_DIMENSIONS
1239
+ );
1240
+ if (quote.status === "active") {
1241
+ insertSearch.run(
1242
+ quote.quoteId,
1243
+ quote.text,
1244
+ quote.tags.join(" "),
1245
+ quote.source ?? "",
1246
+ quote.attribution ?? ""
1247
+ );
1248
+ }
1249
+ }
1250
+ const archiveMissing = database.prepare(`
1251
+ UPDATE quotes
1252
+ SET status = 'archived', updated_at = ?, embedding = NULL, embedding_model = NULL,
1253
+ embedding_revision = NULL, embedding_quantization = NULL, embedding_dimensions = NULL
1254
+ WHERE status = 'active' AND quote_id NOT IN (${quotes.map(() => "?").join(",") || "''"})
1255
+ `);
1256
+ archiveMissing.run(timestamp, ...sourceIds);
1257
+ database.prepare(
1258
+ `INSERT INTO quote_sources (source_key, content_sha256, synced_at) VALUES ('default', ?, ?)
1259
+ ON CONFLICT(source_key) DO UPDATE SET content_sha256 = excluded.content_sha256, synced_at = excluded.synced_at`
1260
+ ).run(sourceSha256, timestamp);
1261
+ database.exec("COMMIT");
1262
+ } catch {
1263
+ try {
1264
+ database.exec("ROLLBACK");
1265
+ } catch {
1266
+ }
1267
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_IO_FAILED", "Unable to synchronize quote index");
1268
+ }
1269
+ return priorSource === void 0 ? "created" : "updated";
1270
+ }
1271
+ function quoteSemanticIndexComplete(database) {
1272
+ const row = sqlRow(
1273
+ database.prepare(
1274
+ `SELECT COUNT(*) AS total, COALESCE(SUM(CASE
1275
+ WHEN embedding IS NOT NULL AND embedding_model = ? AND embedding_revision = ?
1276
+ AND embedding_quantization = ? AND embedding_dimensions = ? THEN 1 ELSE 0 END), 0) AS indexed
1277
+ FROM quotes WHERE status = 'active'`
1278
+ ).get(
1279
+ CONTENT_KNOWLEDGE_EMBEDDING_MODEL,
1280
+ CONTENT_KNOWLEDGE_EMBEDDING_REVISION,
1281
+ CONTENT_KNOWLEDGE_EMBEDDING_QUANTIZATION,
1282
+ CONTENT_KNOWLEDGE_EMBEDDING_DIMENSIONS
1283
+ )
1284
+ );
1285
+ return numberColumn(row, "total") === numberColumn(row, "indexed");
1286
+ }
764
1287
  function embeddingText(article, chunk) {
765
1288
  return [article.title.slice(0, 120), chunk.heading?.slice(0, 120) ?? "", chunk.body].filter((value) => value.length > 0).join("\n");
766
1289
  }
@@ -972,6 +1495,28 @@ function loadChunks(database) {
972
1495
  CONTENT_KNOWLEDGE_EMBEDDING_DIMENSIONS
973
1496
  ).map(chunkRow);
974
1497
  }
1498
+ function loadActiveQuotes(database) {
1499
+ return database.prepare(
1500
+ `
1501
+ SELECT
1502
+ quote_id, status, body, tags_json, source_label, origin, attribution, verbatim_use,
1503
+ content_sha256,
1504
+ CASE
1505
+ WHEN embedding_model = ? AND embedding_revision = ? AND embedding_quantization = ?
1506
+ AND embedding_dimensions = ?
1507
+ THEN embedding ELSE NULL
1508
+ END AS embedding
1509
+ FROM quotes
1510
+ WHERE status = 'active'
1511
+ ORDER BY imported_at ASC, quote_id ASC
1512
+ `
1513
+ ).all(
1514
+ CONTENT_KNOWLEDGE_EMBEDDING_MODEL,
1515
+ CONTENT_KNOWLEDGE_EMBEDDING_REVISION,
1516
+ CONTENT_KNOWLEDGE_EMBEDDING_QUANTIZATION,
1517
+ CONTENT_KNOWLEDGE_EMBEDDING_DIMENSIONS
1518
+ ).map(quoteRow);
1519
+ }
975
1520
  function ftsExpression(query) {
976
1521
  const terms = query.normalize("NFKC").split(/[\s,,。!?!?;;::、/\\|()[\]{}]+/u).map((term) => term.trim()).filter((term) => [...term].length >= 3).slice(0, 12);
977
1522
  if (terms.length === 0) {
@@ -1001,6 +1546,24 @@ function lexicalRanking(database, query) {
1001
1546
  throw new KnowledgeCliError("DXC_KNOWLEDGE_QUERY_INVALID", "Knowledge query is invalid");
1002
1547
  }
1003
1548
  }
1549
+ function quoteLexicalRanking(database, query) {
1550
+ const expression = ftsExpression(query);
1551
+ if (expression === null) {
1552
+ return [];
1553
+ }
1554
+ try {
1555
+ return database.prepare(
1556
+ `
1557
+ SELECT quote_id FROM quote_search
1558
+ WHERE quote_search MATCH ?
1559
+ ORDER BY bm25(quote_search, 4.0, 1.5, 0.5, 0.5, 0.5)
1560
+ LIMIT ?
1561
+ `
1562
+ ).all(expression, SEARCH_CANDIDATE_LIMIT).map((value, rank) => ({ id: stringColumn(sqlRow(value), "quote_id"), rank }));
1563
+ } catch {
1564
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUERY_INVALID", "Knowledge query is invalid");
1565
+ }
1566
+ }
1004
1567
  function dotProduct(left, right) {
1005
1568
  let result = 0;
1006
1569
  for (let index = 0; index < left.length; index += 1) {
@@ -1009,9 +1572,7 @@ function dotProduct(left, right) {
1009
1572
  return result;
1010
1573
  }
1011
1574
  function semanticRanking(chunks, queryVector) {
1012
- const candidates = chunks.filter(
1013
- (chunk) => chunk.embedding !== null
1014
- ).map((chunk) => ({
1575
+ const candidates = chunks.filter((chunk) => chunk.embedding !== null).map((chunk) => ({
1015
1576
  id: chunk.id,
1016
1577
  similarity: dotProduct(queryVector, chunk.embedding)
1017
1578
  })).sort((left, right) => right.similarity - left.similarity || left.id.localeCompare(right.id)).slice(0, SEARCH_CANDIDATE_LIMIT);
@@ -1065,6 +1626,7 @@ function fusedResults(chunks, mode, lexical, semantic, limit) {
1065
1626
  rank: results.length + 1,
1066
1627
  snippet: snippet(chunk.body),
1067
1628
  sourceLabel: chunk.sourceLabel,
1629
+ sourceType: "history-article",
1068
1630
  title: chunk.title
1069
1631
  });
1070
1632
  if (results.length >= limit) {
@@ -1073,10 +1635,66 @@ function fusedResults(chunks, mode, lexical, semantic, limit) {
1073
1635
  }
1074
1636
  return results;
1075
1637
  }
1638
+ function fusedQuoteResults(quotes, mode, lexical, semantic, limit) {
1639
+ const scores = /* @__PURE__ */ new Map();
1640
+ const addRanking = (ranking, lane, weight) => {
1641
+ for (const candidate of ranking) {
1642
+ const current = scores.get(candidate.id) ?? { lanes: /* @__PURE__ */ new Set(), score: 0 };
1643
+ current.lanes.add(lane);
1644
+ current.score += weight / (RRF_CONSTANT + candidate.rank + 1);
1645
+ scores.set(candidate.id, current);
1646
+ }
1647
+ };
1648
+ addRanking(semantic, "semantic", mode === "hybrid" ? 0.7 : mode === "semantic" ? 1 : 0);
1649
+ addRanking(lexical, "lexical", mode === "hybrid" ? 0.3 : mode === "lexical" ? 1 : 0);
1650
+ const byId = new Map(quotes.map((quote) => [quote.id, quote]));
1651
+ const results = [];
1652
+ for (const [quoteId, score] of [...scores.entries()].sort(
1653
+ (left, right) => right[1].score - left[1].score || left[0].localeCompare(right[0])
1654
+ )) {
1655
+ const quote = byId.get(quoteId);
1656
+ if (quote === void 0) {
1657
+ continue;
1658
+ }
1659
+ results.push({
1660
+ attribution: quote.attribution,
1661
+ contentSha256: quote.contentSha256,
1662
+ lanes: [...score.lanes].sort(),
1663
+ origin: quote.origin,
1664
+ quoteId: quote.id,
1665
+ rank: results.length + 1,
1666
+ snippet: snippet(quote.text),
1667
+ sourceLabel: quote.sourceLabel,
1668
+ sourceType: "quote",
1669
+ status: quote.status,
1670
+ tags: [...quote.tags],
1671
+ verbatimUse: quote.verbatimUse
1672
+ });
1673
+ if (results.length >= limit) {
1674
+ break;
1675
+ }
1676
+ }
1677
+ return results;
1678
+ }
1679
+ function mergeSearchResults(history, quotes, limit) {
1680
+ const merged = [];
1681
+ for (let index = 0; merged.length < limit && (index < history.length || index < quotes.length); index += 1) {
1682
+ const historyItem = history[index];
1683
+ if (historyItem !== void 0 && merged.length < limit) {
1684
+ merged.push({ ...historyItem, rank: merged.length + 1 });
1685
+ }
1686
+ const quoteItem = quotes[index];
1687
+ if (quoteItem !== void 0 && merged.length < limit) {
1688
+ merged.push({ ...quoteItem, rank: merged.length + 1 });
1689
+ }
1690
+ }
1691
+ return merged;
1692
+ }
1076
1693
  var KnowledgeCli = class {
1077
1694
  #currentDirectory;
1078
1695
  #databasePath;
1079
1696
  #embedder;
1697
+ #homeDirectory;
1080
1698
  #isModelCached;
1081
1699
  #now;
1082
1700
  #randomUUID;
@@ -1085,6 +1703,7 @@ var KnowledgeCli = class {
1085
1703
  const now = dependencies.now ?? (() => /* @__PURE__ */ new Date());
1086
1704
  const modelCacheDirectory = dependencies.modelCacheDirectory ?? path2.join(dependencies.homeDirectory, ".dxc", "models", "bge-small-zh-v1.5");
1087
1705
  this.#currentDirectory = dependencies.currentDirectory;
1706
+ this.#homeDirectory = dependencies.homeDirectory;
1088
1707
  this.#databasePath = dependencies.databasePath ?? path2.join(dependencies.homeDirectory, ".dxc", "content-memory.sqlite");
1089
1708
  this.#embedder = dependencies.embedder ?? new LocalBgeKnowledgeEmbedder(modelCacheDirectory, now);
1090
1709
  this.#isModelCached = dependencies.isModelCached ?? (() => isKnowledgeModelCached(modelCacheDirectory));
@@ -1099,16 +1718,23 @@ var KnowledgeCli = class {
1099
1718
  "At least one article file is required"
1100
1719
  );
1101
1720
  }
1102
- if (!options.lexicalOnly) {
1103
- this.#writeStderr(
1104
- "Preparing the local semantic index; first use downloads a quantized model of about 24 MB.\n"
1105
- );
1106
- }
1107
1721
  const database = await openDatabase(this.#databasePath);
1108
1722
  try {
1109
1723
  const imported = [];
1724
+ const skipped = [];
1725
+ let semanticPreparationAnnounced = false;
1110
1726
  for (const file of options.files) {
1111
- const article = await prepareArticle(this.#currentDirectory, file);
1727
+ let article;
1728
+ try {
1729
+ article = await prepareArticle(this.#currentDirectory, file);
1730
+ } catch (error) {
1731
+ const skippedItem = skippedArticle(error, file);
1732
+ if (skippedItem !== void 0) {
1733
+ skipped.push(skippedItem);
1734
+ continue;
1735
+ }
1736
+ throw error;
1737
+ }
1112
1738
  const existing = findExistingArticle(database, article);
1113
1739
  const unchanged = existing?.contentSha256 === article.contentSha256 && existing.chunkerVersion === CONTENT_KNOWLEDGE_CHUNKER_VERSION;
1114
1740
  const semanticComplete = existing === void 0 ? false : semanticIndexComplete(database, existing.id);
@@ -1120,6 +1746,12 @@ var KnowledgeCli = class {
1120
1746
  });
1121
1747
  continue;
1122
1748
  }
1749
+ if (!options.lexicalOnly && !semanticPreparationAnnounced) {
1750
+ this.#writeStderr(
1751
+ "Preparing the local semantic index; first use downloads a quantized model of about 24 MB.\n"
1752
+ );
1753
+ semanticPreparationAnnounced = true;
1754
+ }
1123
1755
  const vectors = options.lexicalOnly ? void 0 : await embedArticle(this.#embedder, article);
1124
1756
  const saved = saveArticle(
1125
1757
  database,
@@ -1139,7 +1771,8 @@ var KnowledgeCli = class {
1139
1771
  command: "knowledge.import",
1140
1772
  data: {
1141
1773
  imported,
1142
- model: options.lexicalOnly ? null : this.#embedder.model
1774
+ model: options.lexicalOnly ? null : this.#embedder.model,
1775
+ skipped
1143
1776
  },
1144
1777
  ok: true
1145
1778
  });
@@ -1153,6 +1786,236 @@ var KnowledgeCli = class {
1153
1786
  lexicalOnly
1154
1787
  });
1155
1788
  }
1789
+ async initQuotes() {
1790
+ const sourcePath = quoteSourcePath(this.#homeDirectory);
1791
+ const exists = await quoteSourceExists(sourcePath);
1792
+ if (!exists) {
1793
+ const legacyPath = legacyQuoteSourcePath(this.#homeDirectory);
1794
+ const legacyExists = await quoteSourceExists(legacyPath);
1795
+ const quotes = legacyExists ? (await readQuoteSource(legacyPath, true)).quotes : [
1796
+ {
1797
+ quoteId: this.#randomUUID(),
1798
+ status: "active",
1799
+ text: "\u628A\u8FD9\u53E5\u66FF\u6362\u6210\u4F60\u81EA\u5DF1\u7684\u91D1\u53E5\uFF1B\u91D1\u53E5 ID \u4E00\u65E6\u521B\u5EFA\u5C31\u4E0D\u8981\u4FEE\u6539\u3002",
1800
+ tags: ["\u793A\u4F8B"],
1801
+ source: "\u4E2A\u4EBA\u539F\u521B",
1802
+ origin: "original",
1803
+ attribution: null,
1804
+ verbatimUse: "original"
1805
+ }
1806
+ ];
1807
+ await writeQuoteSource(sourcePath, quotes);
1808
+ }
1809
+ const database = await openDatabase(this.#databasePath);
1810
+ try {
1811
+ return contentKnowledgeQuoteInitResultSchema.parse({
1812
+ command: "knowledge.quotes.init",
1813
+ data: { ...quoteSourceSummary(database, true), created: !exists },
1814
+ ok: true
1815
+ });
1816
+ } finally {
1817
+ database.close();
1818
+ }
1819
+ }
1820
+ async listQuotes() {
1821
+ const sourcePath = quoteSourcePath(this.#homeDirectory);
1822
+ if (!await quoteSourceExists(sourcePath)) {
1823
+ throw new KnowledgeCliError(
1824
+ "DXC_KNOWLEDGE_QUOTE_SOURCE_NOT_INITIALIZED",
1825
+ "Quote source file is not initialized; run knowledge quotes init first"
1826
+ );
1827
+ }
1828
+ const source = await readQuoteSource(sourcePath);
1829
+ return contentKnowledgeQuoteListResultSchema.parse({
1830
+ command: "knowledge.quotes.list",
1831
+ data: { quotes: source.quotes.map(quoteRecord) },
1832
+ ok: true
1833
+ });
1834
+ }
1835
+ async addQuote(options) {
1836
+ const sourcePath = quoteSourcePath(this.#homeDirectory);
1837
+ if (!await quoteSourceExists(sourcePath)) {
1838
+ throw new KnowledgeCliError(
1839
+ "DXC_KNOWLEDGE_QUOTE_SOURCE_NOT_INITIALIZED",
1840
+ "Quote source file is not initialized; run knowledge quotes init first"
1841
+ );
1842
+ }
1843
+ const source = await readQuoteSource(sourcePath);
1844
+ const quote = quoteSourceItemSchema.safeParse({
1845
+ attribution: options.attribution ?? null,
1846
+ origin: options.origin ?? "original",
1847
+ quoteId: this.#randomUUID(),
1848
+ source: options.source ?? null,
1849
+ status: "active",
1850
+ tags: options.tags ?? [],
1851
+ text: options.text,
1852
+ verbatimUse: options.verbatimUse ?? (options.origin === "third-party" ? "needs-verification" : "original")
1853
+ });
1854
+ if (!quote.success) {
1855
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote input is invalid");
1856
+ }
1857
+ await writeQuoteSource(sourcePath, [...source.quotes, quote.data]);
1858
+ return contentKnowledgeQuoteMutationResultSchema.parse({
1859
+ command: "knowledge.quotes.add",
1860
+ data: { pendingSync: true, quote: quoteRecord(quote.data) },
1861
+ ok: true
1862
+ });
1863
+ }
1864
+ async updateQuote(quoteId, options) {
1865
+ if (!UUID_PATTERN.test(quoteId) || Object.keys(options).length === 0) {
1866
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote update is invalid");
1867
+ }
1868
+ const sourcePath = quoteSourcePath(this.#homeDirectory);
1869
+ if (!await quoteSourceExists(sourcePath)) {
1870
+ throw new KnowledgeCliError(
1871
+ "DXC_KNOWLEDGE_QUOTE_SOURCE_NOT_INITIALIZED",
1872
+ "Quote source file is not initialized; run knowledge quotes init first"
1873
+ );
1874
+ }
1875
+ const source = await readQuoteSource(sourcePath);
1876
+ const index = source.quotes.findIndex((quote) => quote.quoteId === quoteId);
1877
+ if (index < 0) {
1878
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote ID was not found");
1879
+ }
1880
+ const existing = source.quotes[index];
1881
+ if (existing === void 0) {
1882
+ throw new KnowledgeCliError(
1883
+ "DXC_KNOWLEDGE_STATE_INVALID",
1884
+ "Knowledge quote state is invalid"
1885
+ );
1886
+ }
1887
+ const updated = quoteSourceItemSchema.safeParse({
1888
+ ...sourceQuote(existing),
1889
+ ...options,
1890
+ attribution: options.attribution === void 0 ? existing.attribution ?? null : options.attribution,
1891
+ source: options.source === void 0 ? existing.source ?? null : options.source,
1892
+ tags: options.tags === void 0 ? existing.tags : options.tags,
1893
+ verbatimUse: options.verbatimUse ?? (options.origin === "third-party" && existing.origin !== "third-party" ? "needs-verification" : existing.verbatimUse)
1894
+ });
1895
+ if (!updated.success) {
1896
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote update is invalid");
1897
+ }
1898
+ const quotes = source.quotes.map(sourceQuote);
1899
+ quotes[index] = updated.data;
1900
+ await writeQuoteSource(sourcePath, quotes);
1901
+ return contentKnowledgeQuoteMutationResultSchema.parse({
1902
+ command: "knowledge.quotes.update",
1903
+ data: { pendingSync: true, quote: quoteRecord(updated.data) },
1904
+ ok: true
1905
+ });
1906
+ }
1907
+ async archiveQuote(quoteId) {
1908
+ if (!UUID_PATTERN.test(quoteId)) {
1909
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote ID is invalid");
1910
+ }
1911
+ const sourcePath = quoteSourcePath(this.#homeDirectory);
1912
+ if (!await quoteSourceExists(sourcePath)) {
1913
+ throw new KnowledgeCliError(
1914
+ "DXC_KNOWLEDGE_QUOTE_SOURCE_NOT_INITIALIZED",
1915
+ "Quote source file is not initialized; run knowledge quotes init first"
1916
+ );
1917
+ }
1918
+ const source = await readQuoteSource(sourcePath);
1919
+ const index = source.quotes.findIndex((quote) => quote.quoteId === quoteId);
1920
+ if (index < 0) {
1921
+ throw new KnowledgeCliError("DXC_KNOWLEDGE_QUOTE_INVALID", "Quote ID was not found");
1922
+ }
1923
+ const existing = source.quotes[index];
1924
+ if (existing === void 0) {
1925
+ throw new KnowledgeCliError(
1926
+ "DXC_KNOWLEDGE_STATE_INVALID",
1927
+ "Knowledge quote state is invalid"
1928
+ );
1929
+ }
1930
+ const archived = { ...sourceQuote(existing), status: "archived" };
1931
+ const quotes = source.quotes.map(sourceQuote);
1932
+ quotes[index] = archived;
1933
+ await writeQuoteSource(sourcePath, quotes);
1934
+ return contentKnowledgeQuoteMutationResultSchema.parse({
1935
+ command: "knowledge.quotes.archive",
1936
+ data: { pendingSync: true, quote: quoteRecord(archived) },
1937
+ ok: true
1938
+ });
1939
+ }
1940
+ async quoteStatus() {
1941
+ const sourceInitialized = await quoteSourceExists(quoteSourcePath(this.#homeDirectory));
1942
+ if (!await databaseExists(this.#databasePath)) {
1943
+ return contentKnowledgeQuoteStatusResultSchema.parse({
1944
+ command: "knowledge.quotes.status",
1945
+ data: {
1946
+ activeCount: 0,
1947
+ archivedCount: 0,
1948
+ sourceInitialized,
1949
+ sourcePath: QUOTE_SOURCE_DISPLAY_PATH,
1950
+ sourceSha256: null
1951
+ },
1952
+ ok: true
1953
+ });
1954
+ }
1955
+ const database = await openDatabase(this.#databasePath);
1956
+ try {
1957
+ return contentKnowledgeQuoteStatusResultSchema.parse({
1958
+ command: "knowledge.quotes.status",
1959
+ data: quoteSourceSummary(database, sourceInitialized),
1960
+ ok: true
1961
+ });
1962
+ } finally {
1963
+ database.close();
1964
+ }
1965
+ }
1966
+ async syncQuotes(lexicalOnly) {
1967
+ const sourcePath = quoteSourcePath(this.#homeDirectory);
1968
+ if (!await quoteSourceExists(sourcePath)) {
1969
+ throw new KnowledgeCliError(
1970
+ "DXC_KNOWLEDGE_QUOTE_SOURCE_NOT_INITIALIZED",
1971
+ "Quote source file is not initialized; run knowledge quotes init first"
1972
+ );
1973
+ }
1974
+ if (!lexicalOnly) {
1975
+ this.#writeStderr(
1976
+ "Preparing the local semantic quote index; first use downloads a quantized model of about 24 MB.\n"
1977
+ );
1978
+ }
1979
+ const source = await readQuoteSource(sourcePath);
1980
+ const database = await openDatabase(this.#databasePath);
1981
+ try {
1982
+ const sourceRow = database.prepare("SELECT content_sha256 FROM quote_sources WHERE source_key = 'default' LIMIT 1").get();
1983
+ const unchanged = sourceRow !== void 0 && stringColumn(sqlRow(sourceRow), "content_sha256") === source.contentSha256 && (lexicalOnly || quoteSemanticIndexComplete(database));
1984
+ const currentQuotes = new Map(loadActiveQuotes(database).map((quote) => [quote.id, quote]));
1985
+ const quotesToEmbed = source.quotes.filter((quote) => {
1986
+ const current = currentQuotes.get(quote.quoteId);
1987
+ return quote.status === "active" && (current?.contentSha256 !== quote.contentSha256 || current.embedding === null);
1988
+ });
1989
+ const vectors = unchanged || lexicalOnly ? void 0 : await embedQuotes(this.#embedder, quotesToEmbed);
1990
+ if (vectors !== void 0) {
1991
+ for (const quote of source.quotes) {
1992
+ const current = currentQuotes.get(quote.quoteId);
1993
+ if (quote.status === "active" && !vectors.has(quote.quoteId) && current?.contentSha256 === quote.contentSha256 && current.embedding !== null) {
1994
+ vectors.set(quote.quoteId, current.embedding);
1995
+ }
1996
+ }
1997
+ }
1998
+ const status = syncQuotes(
1999
+ database,
2000
+ source.quotes,
2001
+ source.contentSha256,
2002
+ vectors,
2003
+ this.#now().toISOString(),
2004
+ unchanged
2005
+ );
2006
+ return contentKnowledgeQuoteSyncResultSchema.parse({
2007
+ command: "knowledge.quotes.sync",
2008
+ data: {
2009
+ ...quoteSourceSummary(database, true),
2010
+ model: lexicalOnly ? null : this.#embedder.model,
2011
+ status
2012
+ },
2013
+ ok: true
2014
+ });
2015
+ } finally {
2016
+ database.close();
2017
+ }
2018
+ }
1156
2019
  async remove(articleId) {
1157
2020
  if (!UUID_PATTERN.test(articleId)) {
1158
2021
  throw new KnowledgeCliError(
@@ -1214,16 +2077,19 @@ var KnowledgeCli = class {
1214
2077
  const database = await openDatabase(this.#databasePath);
1215
2078
  try {
1216
2079
  const chunks = loadChunks(database);
2080
+ const quotes = loadActiveQuotes(database);
1217
2081
  const usesSemantic = mode.data !== "lexical";
1218
- if (usesSemantic && chunks.some((chunk) => chunk.embedding === null)) {
2082
+ if (usesSemantic && (chunks.some((chunk) => chunk.embedding === null) || quotes.some((quote) => quote.embedding === null))) {
1219
2083
  throw new KnowledgeCliError(
1220
2084
  "DXC_KNOWLEDGE_SEMANTIC_NOT_READY",
1221
- "All articles must have a semantic index before hybrid search"
2085
+ "All active knowledge items must have a semantic index before hybrid search"
1222
2086
  );
1223
2087
  }
1224
2088
  const lexical = mode.data === "semantic" ? [] : lexicalRanking(database, query);
2089
+ const quoteLexical = mode.data === "semantic" ? [] : quoteLexicalRanking(database, query);
1225
2090
  let semantic = [];
1226
- if (usesSemantic && chunks.length > 0) {
2091
+ let quoteSemantic = [];
2092
+ if (usesSemantic && (chunks.length > 0 || quotes.length > 0)) {
1227
2093
  let queryVector;
1228
2094
  try {
1229
2095
  queryVector = await this.#embedder.embedQuery(query);
@@ -1234,6 +2100,7 @@ var KnowledgeCli = class {
1234
2100
  );
1235
2101
  }
1236
2102
  semantic = semanticRanking(chunks, queryVector);
2103
+ quoteSemantic = semanticRanking(quotes, queryVector);
1237
2104
  }
1238
2105
  return contentKnowledgeSearchResultSchema.parse({
1239
2106
  command: "knowledge.search",
@@ -1241,7 +2108,11 @@ var KnowledgeCli = class {
1241
2108
  mode: mode.data,
1242
2109
  model: usesSemantic ? this.#embedder.model : null,
1243
2110
  query,
1244
- results: fusedResults(chunks, mode.data, lexical, semantic, limitInput)
2111
+ results: mergeSearchResults(
2112
+ fusedResults(chunks, mode.data, lexical, semantic, limitInput),
2113
+ fusedQuoteResults(quotes, mode.data, quoteLexical, quoteSemantic, limitInput),
2114
+ limitInput
2115
+ )
1245
2116
  },
1246
2117
  ok: true
1247
2118
  });
@@ -1253,6 +2124,13 @@ var KnowledgeCli = class {
1253
2124
  let articleCount = 0;
1254
2125
  let chunkCount = 0;
1255
2126
  let semanticIndexedChunkCount = 0;
2127
+ let quoteSummary = {
2128
+ activeCount: 0,
2129
+ archivedCount: 0,
2130
+ sourceInitialized: await quoteSourceExists(quoteSourcePath(this.#homeDirectory)),
2131
+ sourcePath: QUOTE_SOURCE_DISPLAY_PATH,
2132
+ sourceSha256: null
2133
+ };
1256
2134
  if (await databaseExists(this.#databasePath)) {
1257
2135
  const database = await openDatabase(this.#databasePath);
1258
2136
  try {
@@ -1287,6 +2165,7 @@ var KnowledgeCli = class {
1287
2165
  articleCount = numberColumn(articleRowValue, "count");
1288
2166
  chunkCount = numberColumn(chunkRowValue, "count");
1289
2167
  semanticIndexedChunkCount = numberColumn(chunkRowValue, "semantic_count");
2168
+ quoteSummary = quoteSourceSummary(database, quoteSummary.sourceInitialized);
1290
2169
  } finally {
1291
2170
  database.close();
1292
2171
  }
@@ -1304,7 +2183,8 @@ var KnowledgeCli = class {
1304
2183
  revision: CONTENT_KNOWLEDGE_EMBEDDING_REVISION
1305
2184
  }),
1306
2185
  modelCached: await this.#isModelCached(),
1307
- semanticIndexedChunkCount
2186
+ semanticIndexedChunkCount,
2187
+ quotes: quoteSummary
1308
2188
  },
1309
2189
  ok: true
1310
2190
  });