@v587d/capital-generation 2.8.3 → 2.9.1

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 (169) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.md +40 -19
  3. package/chart-ui/README.md +5 -4
  4. package/chart-ui/client.src.cjs +7 -16
  5. package/chart-ui/index.js +12 -33
  6. package/chart-ui/package.json +2 -2
  7. package/config/client.js +18 -39
  8. package/config/client.src.cjs +79 -217
  9. package/config/index.js +22 -72
  10. package/config/package.json +2 -2
  11. package/cordis.patch.yml +46 -51
  12. package/lib/agents/bash-guard.d.ts +9 -55
  13. package/lib/agents/bash-guard.js +27 -67
  14. package/lib/agents/preset-roster.d.ts +22 -0
  15. package/lib/agents/preset-roster.js +87 -0
  16. package/lib/agents/role-tool-policy.d.ts +46 -0
  17. package/lib/agents/role-tool-policy.js +205 -0
  18. package/lib/agents/root-tool-policy.d.ts +28 -124
  19. package/lib/agents/root-tool-policy.js +164 -237
  20. package/lib/agents/selected-skills-provider.d.ts +2 -6
  21. package/lib/agents/selected-skills-provider.js +27 -84
  22. package/lib/capital-settings.d.ts +27 -0
  23. package/lib/capital-settings.js +23 -0
  24. package/lib/chart/artifact-ref.d.ts +13 -8
  25. package/lib/chart/artifact-ref.js +27 -10
  26. package/lib/chart/downsample.d.ts +2 -5
  27. package/lib/chart/downsample.js +3 -8
  28. package/lib/chart/errors.d.ts +3 -7
  29. package/lib/chart/errors.js +1 -2
  30. package/lib/chart/event-context.d.ts +8 -0
  31. package/lib/chart/event-context.js +39 -0
  32. package/lib/chart/events.d.ts +21 -78
  33. package/lib/chart/events.js +43 -189
  34. package/lib/chart/html.d.ts +2 -5
  35. package/lib/chart/html.js +8 -15
  36. package/lib/chart/present-tool.d.ts +28 -0
  37. package/lib/chart/present-tool.js +128 -0
  38. package/lib/chart/runtime.d.ts +5 -18
  39. package/lib/chart/runtime.js +14 -34
  40. package/lib/chart/series.d.ts +10 -17
  41. package/lib/chart/series.js +22 -47
  42. package/lib/chart/source-token.d.ts +5 -13
  43. package/lib/chart/source-token.js +18 -17
  44. package/lib/chart/spec.d.ts +6 -12
  45. package/lib/chart/spec.js +7 -31
  46. package/lib/chart/tool.d.ts +7 -29
  47. package/lib/chart/tool.js +15 -25
  48. package/lib/chart-render-entry.d.ts +10 -0
  49. package/lib/chart-render-entry.js +14 -0
  50. package/lib/chart-source-entry.d.ts +13 -0
  51. package/lib/chart-source-entry.js +20 -0
  52. package/lib/collect-entry.d.ts +9 -0
  53. package/lib/collect-entry.js +10 -0
  54. package/lib/collector-runtime.d.ts +22 -0
  55. package/lib/collector-runtime.js +97 -0
  56. package/lib/data-collector/args.d.ts +6 -24
  57. package/lib/data-collector/args.js +6 -24
  58. package/lib/data-collector/dataset-tools.d.ts +7 -1
  59. package/lib/data-collector/dataset-tools.js +27 -56
  60. package/lib/data-collector/describe.d.ts +8 -33
  61. package/lib/data-collector/describe.js +17 -70
  62. package/lib/data-collector/hub.d.ts +27 -85
  63. package/lib/data-collector/hub.js +19 -85
  64. package/lib/data-collector/query-time.d.ts +7 -26
  65. package/lib/data-collector/query-time.js +8 -27
  66. package/lib/data-collector/store.d.ts +41 -117
  67. package/lib/data-collector/store.js +32 -111
  68. package/lib/data-collector/time-axis.d.ts +16 -76
  69. package/lib/data-collector/time-axis.js +25 -111
  70. package/lib/data-collector/tools.d.ts +6 -6
  71. package/lib/data-collector/tools.js +34 -43
  72. package/lib/dataset-read-entry.d.ts +9 -0
  73. package/lib/dataset-read-entry.js +7 -0
  74. package/lib/index.d.ts +7 -117
  75. package/lib/index.js +43 -422
  76. package/lib/net/eastmoney-client.d.ts +13 -65
  77. package/lib/net/eastmoney-client.js +12 -53
  78. package/lib/net/throttle.d.ts +2 -13
  79. package/lib/net/throttle.js +3 -14
  80. package/lib/ocr/client.d.ts +12 -21
  81. package/lib/ocr/client.js +23 -40
  82. package/lib/ocr/markdown.d.ts +20 -26
  83. package/lib/ocr/markdown.js +23 -36
  84. package/lib/ocr/store.d.ts +14 -47
  85. package/lib/ocr/store.js +15 -45
  86. package/lib/ocr-entry.d.ts +9 -0
  87. package/lib/ocr-entry.js +10 -0
  88. package/lib/policy-entry.d.ts +17 -0
  89. package/lib/policy-entry.js +22 -0
  90. package/lib/present-entry.d.ts +9 -0
  91. package/lib/present-entry.js +13 -0
  92. package/lib/preset-claim.d.ts +22 -0
  93. package/lib/preset-claim.js +24 -0
  94. package/lib/preset-ids.d.ts +38 -0
  95. package/lib/preset-ids.js +46 -0
  96. package/lib/retriever-config.d.ts +37 -0
  97. package/lib/retriever-config.js +36 -0
  98. package/lib/retriever-runtime.d.ts +31 -0
  99. package/lib/retriever-runtime.js +82 -0
  100. package/lib/root-extras-entry.d.ts +17 -0
  101. package/lib/root-extras-entry.js +25 -0
  102. package/lib/runtime-entry.d.ts +15 -0
  103. package/lib/runtime-entry.js +5 -0
  104. package/lib/runtime.d.ts +21 -0
  105. package/lib/runtime.js +33 -0
  106. package/lib/search-entry.d.ts +9 -0
  107. package/lib/search-entry.js +9 -0
  108. package/lib/sources/eastmoney-http.js +20 -68
  109. package/lib/sources/ftshare-rest.d.ts +6 -34
  110. package/lib/sources/ftshare-rest.js +84 -292
  111. package/lib/sources/fuyao-core.d.ts +8 -18
  112. package/lib/sources/fuyao-core.js +10 -21
  113. package/lib/sources/fuyao-rest.d.ts +2 -6
  114. package/lib/sources/fuyao-rest.js +23 -102
  115. package/lib/sources/security-code.d.ts +6 -12
  116. package/lib/sources/security-code.js +6 -12
  117. package/lib/sources/security-identity.d.ts +1 -4
  118. package/lib/sources/security-identity.js +3 -8
  119. package/lib/sources/tencent-http.js +14 -28
  120. package/lib/sources/tencent-public-core.d.ts +8 -60
  121. package/lib/sources/tencent-public-core.js +12 -77
  122. package/lib/sources/wind-mcp.d.ts +0 -5
  123. package/lib/sources/wind-mcp.js +23 -78
  124. package/lib/sources-entry.d.ts +9 -0
  125. package/lib/sources-entry.js +9 -0
  126. package/lib/time/tools.d.ts +8 -31
  127. package/lib/time/tools.js +30 -66
  128. package/lib/time-entry.d.ts +9 -0
  129. package/lib/time-entry.js +7 -0
  130. package/lib/tool-exec.d.ts +45 -11
  131. package/lib/tool-exec.js +59 -6
  132. package/lib/user-customization.d.ts +14 -0
  133. package/lib/user-customization.js +47 -0
  134. package/lib/watchlist/tools.d.ts +3 -13
  135. package/lib/watchlist/tools.js +10 -30
  136. package/lib/watchlist-entry.d.ts +9 -0
  137. package/lib/watchlist-entry.js +6 -0
  138. package/lib/web-retriever/engines.d.ts +6 -11
  139. package/lib/web-retriever/engines.js +9 -14
  140. package/lib/web-retriever/html-markdown.d.ts +14 -34
  141. package/lib/web-retriever/html-markdown.js +39 -75
  142. package/lib/web-retriever/local-fetch.d.ts +22 -58
  143. package/lib/web-retriever/local-fetch.js +33 -77
  144. package/lib/web-retriever/retriever.d.ts +6 -18
  145. package/lib/web-retriever/retriever.js +7 -7
  146. package/lib/web-retriever/sources.d.ts +37 -124
  147. package/lib/web-retriever/sources.js +64 -196
  148. package/lib/web-retriever/tools.d.ts +43 -27
  149. package/lib/web-retriever/tools.js +99 -137
  150. package/lib/web-retriever/wind-client.d.ts +14 -31
  151. package/lib/web-retriever/wind-client.js +16 -32
  152. package/locale/zh.json +1 -1
  153. package/package.json +72 -7
  154. package/preset/capital-generation/README.md +87 -53
  155. package/preset/capital-generation/agent.patch.yml +532 -337
  156. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -4
  157. package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
  158. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
  159. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +4 -2
  160. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
  161. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +30 -33
  162. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
  163. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +14 -16
  164. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
  165. package/watchlist/client.js +15 -148
  166. package/watchlist/client.src.cjs +148 -662
  167. package/watchlist/holding-rules.js +11 -38
  168. package/watchlist/index.js +127 -397
  169. package/watchlist/package.json +2 -2
@@ -1,16 +1,11 @@
1
1
  /**
2
- * PaddleOCR 输出 → 模型可读 markdown 的归一化(`ocr` 的呈现层,唯一一份实现)。
3
- *
4
- * 为什么必须归一化(2026-09-25 拿真报文实测,贵州茅台 15 页研报):上游把**表格输出成内联
5
- * HTML**,每个单元格都带 `style='text-align: center; word-wrap: break-word;'`——
6
- * 105,639 个字符里 63,232 个(60%)是 `style='...'` 样板,去掉再转成 GFM 管道表只剩 27,156。
7
- * 本仓的场景恰恰是"大量表格、图形",不处理就等于**预算的六成买的是 HTML 属性**,
8
- * 而 `prettifyMarkdown` 这类 flag 在上游被静默忽略(未知键也返回 Success),帮不上。
9
- *
10
- * 归一化只做**无损排版**:不删正文、不改数字、不做摘要。数字一律照抄上游。
2
+ * PaddleOCR output -> model-readable markdown (the only implementation). Upstream emits
3
+ * tables as inline HTML with per-cell `style=`, so on a measured 15-page report ~60% of
4
+ * characters were style boilerplate; `prettifyMarkdown`-style flags are silently ignored.
5
+ * Normalization is lossless layout only: no text, numbers or tables are dropped.
11
6
  */
12
7
  const PAGE_MARKER_PREFIX = '<!-- page:';
13
- /** HTML 实体:财报文档里 `&amp;` 出现在公司名与科目名里,不解开就是脏数据。 */
8
+ /** `&amp;` appears in company and account names; leaving it encoded is dirty data. */
14
9
  const NAMED_ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
15
10
  function decodeEntities(value) {
16
11
  return value.replace(/&(#\d+|#x[0-9a-f]+|[a-z]+);/giu, (whole, body) => {
@@ -32,27 +27,22 @@ function stripTags(value) {
32
27
  .trim();
33
28
  }
34
29
  function escapeCell(value) {
35
- // GFM 单元格里的竖线必须转义,否则一行被切成更多列——表格串列比报错危险(声明与正文不一致)。
30
+ // GFM cells need escaped pipes; a split row misaligns columns silently.
36
31
  return value.replace(/\|/gu, '\\|');
37
32
  }
38
- /** 取一行 `<tr>` 里的单元格文本。 */
33
+ /** Cell text from one `<tr>`; split on open tags, then cut at the matching close tag. */
39
34
  function rowCells(row) {
40
35
  const cells = [];
41
- // 先按**开**标签切:畸形行(少写 `</td>`,实测存在)也能一格一切;再按闭标签截断本段,
42
- // 防止 `1</td><td>2` 这类残留把下一格的内容并进来。第一格之前的 `<thead>` 碎片一律丢弃。
43
- // ⚠️ 单元格标签是 `<td>` **或** `<th>`(上游表头行用 `<th>`,实测):写成 `<t(?:d|th)`
44
- // 只匹配得到 `<td`,`<th>` 行切不出格、整行被 `kept` 丢掉——表头静默消失。
45
- // `\b` 在这里是必要的:它让 `<thead>` / `<table>` 不被误当成单元格开标签。
36
+ // Upstream header rows use `<th>`; matching only `<td` silently drops whole header rows.
37
+ // `\b` keeps `<thead>` / `<table>` from being read as cell openers.
46
38
  for (const segment of row.split(/<t(?:d|h)\b[^>]*>/giu).slice(1)) {
47
39
  cells.push(stripTags(segment.split(/<\/t(?:d|h)\s*>/iu)[0] ?? ''));
48
40
  }
49
41
  return cells;
50
42
  }
51
43
  /**
52
- * 一张 HTML 表 → GFM 管道表。
53
- *
54
- * 表头按上游第一行处理(上游会把 `<thead><th>` 放第一行);列数以各行最大值为准并补齐,
55
- * 因为 GFM 要求分隔行列数与表头一致,少一格整张表退化成裸文本。
44
+ * HTML table -> GFM pipe table. Header is upstream's first row; pad every row to the
45
+ * widest so GFM's separator count matches the header (a short row degrades the table).
56
46
  */
57
47
  function tableToMarkdown(tableHtml) {
58
48
  const rows = [...tableHtml.matchAll(/<tr\b[^>]*>([\s\S]*?)(?:<\/tr>|$)/giu)].map((match) => rowCells(match[1]));
@@ -64,7 +54,7 @@ function tableToMarkdown(tableHtml) {
64
54
  lines.splice(1, 0, `|${' --- |'.repeat(width)}`);
65
55
  return lines.join('\n');
66
56
  }
67
- /** 剥样式 + HTML 表转 GFM + 收空行。对不含 HTML 的正文近似恒等。 */
57
+ /** Strip styles, convert HTML tables to GFM, collapse blank lines; near-identity otherwise. */
68
58
  export function normalizeOcrMarkdown(source) {
69
59
  const styleMatches = source.match(/\s(?:style|class)\s*=\s*(?:"[^"]*"|'[^']*')/giu) ?? [];
70
60
  const styleCharsRemoved = styleMatches.reduce((total, match) => total + match.length, 0);
@@ -77,8 +67,8 @@ export function normalizeOcrMarkdown(source) {
77
67
  .replace(/<br\s*\/?\s*>/giu, ' ')
78
68
  .replace(/<\/?(?:td|th|tr|thead|tbody|tfoot|table|div|span|p|font|img|a|sup|sub|b|i|u|s|em|strong)\b[^>]*>/giu, '')
79
69
  .replace(/\s(?:style|class)\s*=\s*(?:"[^"]*"|'[^']*')/giu, '')
80
- // 上游会把标题粘在正文尾巴上(实测 `2024-11-12## 盈利预测`),标题不在行首就没法进索引;
81
- // 要求 `#` 后必须有空格,避免把 "评级#买入" 这类没有空格的井号误拆。
70
+ // Upstream glues headings to preceding text (`2024-11-12## 盈利预测`); require a
71
+ // space after `#` so "评级#买入" is not split.
82
72
  .replace(/(?<=\S)(#{1,6}\s+\S)/gu, '\n\n$1')
83
73
  .replace(/[ \t]+\n/gu, '\n')
84
74
  .replace(/\n{3,}/gu, '\n\n');
@@ -92,7 +82,7 @@ function firstHeading(markdown) {
92
82
  }
93
83
  return undefined;
94
84
  }
95
- /** 把逐页 markdown 拼成一份文档,并算出**页偏移索引**(按页取回与关键词定位都靠它)。 */
85
+ /** Join per-page markdown and record page offsets, used by page reads and keyword lookup. */
96
86
  export function buildOcrDocument(sourcePages) {
97
87
  const chunks = [];
98
88
  const pages = [];
@@ -127,8 +117,8 @@ function pageOf(pages, at) {
127
117
  return pages.find((entry) => at >= entry.start && at < entry.end) ?? pages[pages.length - 1];
128
118
  }
129
119
  /**
130
- * 页区间解析:`"1-3"` / `"5"` / `"2-4,9"`(1 起始,闭区间)。
131
- * 越界页码**响亮失败**而不是静默少给——"这页没有"和"我没给"在回执里必须能区分。
120
+ * `"1-3"` / `"5"` / `"2-4,9"` (1-based, inclusive). Out-of-range pages fail loudly:
121
+ * "page absent" and "not requested" must stay distinguishable in the receipt.
132
122
  */
133
123
  export function parsePageRange(spec, totalPages) {
134
124
  const taken = [];
@@ -159,7 +149,7 @@ export function parsePageRange(spec, totalPages) {
159
149
  throw new Error(`pages is empty after normalization: '${spec}'`);
160
150
  return taken;
161
151
  }
162
- /** 按页切出正文(纯字符串切片,不重跑上游)。 */
152
+ /** Slice pages out of the stored text; never re-runs upstream. */
163
153
  export function selectPages(document, pages) {
164
154
  return pages.map((page) => {
165
155
  const entry = document.pages.find((item) => item.page === page);
@@ -172,11 +162,10 @@ export function selectPages(document, pages) {
172
162
  });
173
163
  }
174
164
  /**
175
- * 关键词定位:在**已落盘的文档**里找命中,回带页号的上下文片段。
176
- *
177
- * 为什么不靠 `pages` 硬读:一份 15 页研报归一化后约 2.7 万字符,而工具输出预算 6,144——
178
- * 想拿财务预测表就得能"跳着看"。上游页区间不影响算力(整篇都算过),所以定位必须在这份
179
- * 归一化文本上做,不再出网。
165
+ * Keyword lookup over the persisted normalized text (no egress): the output budget is
166
+ * smaller than a whole report, so callers must be able to jump. Adjacent hits merge into
167
+ * one window, and windows never cross a page boundary — a miscited page is worse than
168
+ * fewer hits — which also keeps `<!-- page:N -->` markers out of excerpts.
180
169
  */
181
170
  export function locateQuery(document, query, limit, windowChars) {
182
171
  const haystack = document.document;
@@ -193,12 +182,10 @@ export function locateQuery(document, query, limit, windowChars) {
193
182
  if (at < 0)
194
183
  break;
195
184
  cursor = at + needle.length;
196
- // 相邻命中合并成一段窗口:否则同一句话的三次命中会占满 limit,看着像三条证据其实是一条。
185
+ // Merge adjacent hits: three occurrences of one sentence must not fill `limit`.
197
186
  if (at < lastEnd)
198
187
  continue;
199
188
  const entry = pageOf(document.pages, at);
200
- // 窗口**不越页**:片段被标成"第 N 页"就是要能被引用回第 N 页,跨了页就是错引(错引比少引危险)。
201
- // 顺带把 `<!-- page:N -->` 存储标记挡在外面——那是落盘布局,不是给模型回传正文的东西。
202
189
  const from = Math.max(entry?.start ?? 0, at - windowChars);
203
190
  const to = Math.min(entry?.end ?? haystack.length, at + needle.length + windowChars);
204
191
  hits.push({ page: entry?.page ?? 1, at, excerpt: haystack.slice(from, to).replace(/\n+/gu, ' ⏎ ') });
@@ -1,43 +1,24 @@
1
1
  /**
2
- * `ocr` 的**产物层**:解析结果一律落 `capital-data/ocr/<doc_id>/`,模型侧只拿 `doc_id` 与索引。
3
- *
4
- * 为什么必须落盘而不是留在内存:一份 15 页研报归一化后约 2.7 万字符,而工具输出预算
5
- * 只有 6,144(`SOURCE_OUTPUT_BUDGET_CHARS`)。落盘之后"按页取回 / 关键词定位"才是**纯本地
6
- * 零花费**的第二、第三次读取;否则每次翻页都要重跑上游、再花一次 job。
7
- *
8
- * ```text
9
- * capital-data/ocr/<doc_id>/
10
- * ├── meta.json # 页偏移索引与花费口径(model / charts / job_id / created_at)
11
- * └── document.md # 全篇归一化 markdown,页分隔 `<!-- page:N -->`
12
- * ```
13
- *
14
- * `doc_id` 是**内容寻址**的:`sha1(归一化输入 + model + flags)` 前 12 位。因此"同一份文档
15
- * 重复解析"自动命中缓存不再出网;`refresh` 才强制重跑(上游对同一份 PDF 两次给出过
16
- * 18% 与 19% 的不同数字,重跑**不是**独立验证,引用必须带 `doc_id` 与 `created_at`)。
17
- *
18
- * 写入顺序是 `document.md` → `meta.json`:**meta 的在场即代表产物完整**。中途被中断
19
- * (超时、取消)留下半件产物时,下一次读取按"未缓存"处理并重写(`overwrite`),
20
- * 不会把模型永久锁死在一个坏 `doc_id` 上。
2
+ * OCR artifacts live under `capital-data/ocr/<doc_id>/`; the model only gets `doc_id`.
3
+ * Content-addressed ids make a repeat parse a cache hit. `document.md` is written before
4
+ * `meta.json`, so meta's presence means a complete artifact; half-written ones are redone.
5
+ * Page reads reuse the persisted text instead of re-running a paid upstream job.
21
6
  */
22
7
  import type { SessionLike, WorkspaceDatasetStore } from '../data-collector/store.js';
23
8
  import type { OcrDocument, OcrPageEntry } from './markdown.js';
24
9
  export declare const OCR_ARTIFACT_SUBDIR = "capital-data/ocr";
25
10
  export declare const OCR_DOC_ID_PATTERN: RegExp;
26
- /** 本地文档字节的读取上限:上游 100 页口径下的研报/公告量级,超限宁可报错也不要缓冲整份。 */
11
+ /** Read cap for local documents: 100-page reports fit; fail rather than buffer a whole file. */
27
12
  export declare const OCR_MAX_SOURCE_BYTES: number;
28
- /** 产物元数据(落盘形态)。字段名与回执一致,模型看到的和盘上的不是两份真相。 */
13
+ /** On-disk metadata; field names match the receipt so disk and model see one truth. */
29
14
  export interface OcrArtifactMeta {
30
15
  doc_id: string;
31
- /**
32
- * 输入指认:`url:<http(s) URL>` 或 `file:<workspace 相对路径>`。**绝不落绝对路径**。
33
- * 续查(只传 `job_id` + `doc_id`)时本机没有重算种子的输入,此时**留空而不是编一个**——
34
- * 把作业号写成来源会被当成"这条 URL 就是文档出处"。
35
- */
16
+ /** Input identity, `url:` or `file:` (never absolute); empty on resume — never pass job_id off as provenance. */
36
17
  source?: string;
37
18
  source_kind: 'url' | 'file' | 'job';
38
19
  job_id: string;
39
20
  model: string;
40
- /** 提交时的图表识别开关;续查时无从得知,留空。 */
21
+ /** Chart-recognition flag at submit; unknown on resume, so omitted. */
41
22
  charts?: boolean;
42
23
  pages: number;
43
24
  chars: number;
@@ -51,17 +32,9 @@ export interface OcrArtifact {
51
32
  meta: OcrArtifactMeta;
52
33
  document: OcrDocument;
53
34
  }
54
- /** `seed` 由各形态自己拼(URL 文本 / 文件字节),统一在此收敛成 `ocr_<12 hex>`。 */
55
35
  export declare function deriveOcrDocId(seed: string | Uint8Array, model: string, charts: boolean): string;
56
- /** 回执里给模型的 opaque 引用(与 Dataset/profile 引用同一套 `workspace://` 口径)。 */
57
36
  export declare function ocrArtifactRef(docId: string): string;
58
- /**
59
- * 落盘产物**自己**坏了(调用方没做错任何事)。
60
- *
61
- * 为什么要单独一个类型:翻成回执的那一层要把"下一步该传什么"写清楚,而那一步需要的指认
62
- * (当初解析用过的 url / file)只有这份 meta 里有。让调用方重新去猜链接,等于把一次
63
- * 可机械执行的修复推回给人或模型。
64
- */
37
+ /** Corrupt artifact, not caller error; the repair hint needs the source only this meta holds. */
65
38
  export declare class OcrArtifactError extends Error {
66
39
  readonly reason: 'meta_unreadable' | 'meta_invalid' | 'document_missing';
67
40
  readonly docId: string;
@@ -70,17 +43,15 @@ export declare class OcrArtifactError extends Error {
70
43
  constructor(reason: 'meta_unreadable' | 'meta_invalid' | 'document_missing', docId: string, sourceKind: OcrArtifactMeta['source_kind'] | undefined, source: string | undefined, message: string);
71
44
  }
72
45
  /**
73
- * 读回一份产物。三种"读不到"必须能区分:
74
- * - `undefined`:从未解析过(meta 与 document 都不在)——调用方据此决定提交新 job。
75
- * - 抛错:meta 在但正文丢失/损坏,或半件产物——**不能**当成"没有内容",也不能静默重跑
76
- * (重跑要花一次 job,用户看不到钱去哪了),交给调用方翻成带 `doc_id` 的失败回执。
46
+ * `undefined` = never parsed (safe to submit a job). A throw = meta present but body
47
+ * missing/corrupt: do not treat as empty and do not silently re-run (a re-run costs a job).
77
48
  */
78
49
  export declare function readOcrArtifact(store: WorkspaceDatasetStore, input: {
79
50
  session: SessionLike;
80
51
  doc_id: string;
81
52
  signal?: AbortSignal;
82
53
  }): Promise<OcrArtifact | undefined>;
83
- /** 落盘。`overwrite` 只在调用方**已经决定**重新生成时传(见文件头的写入顺序)。 */
54
+ /** `overwrite` only when the caller has already decided to regenerate. */
84
55
  export declare function writeOcrArtifact(store: WorkspaceDatasetStore, input: {
85
56
  session: SessionLike;
86
57
  meta: OcrArtifactMeta;
@@ -91,7 +62,6 @@ export declare function writeOcrArtifact(store: WorkspaceDatasetStore, input: {
91
62
  artifact_ref: string;
92
63
  paths: string[];
93
64
  }>;
94
- /** 拼一条与盘上记录同形状的 meta(`document` 只提供算好的排版统计)。 */
95
65
  export declare function buildArtifactMeta(input: {
96
66
  doc_id: string;
97
67
  source?: string;
@@ -103,11 +73,8 @@ export declare function buildArtifactMeta(input: {
103
73
  createdAt: string;
104
74
  }): OcrArtifactMeta;
105
75
  /**
106
- * 读取**待解析**的本地文档字节(`file` 形态的入口)。
107
- *
108
- * 只接受 session workspace 内的相对路径:绝对路径(含 Windows 盘符)与 `..` 段在
109
- * `normalizeWorkspaceRelativePath` 就被拒,之后仍要过沙箱归属校验。模型从 `@xxx.pdf`
110
- * mention 里看到的是路径末段,因此回执话术要求它传 workspace 相对路径。
76
+ * `file` entry: workspace-relative only (absolute paths, drive letters and `..` are
77
+ * rejected downstream, then ownership is checked); @mentions show just the path tail.
111
78
  */
112
79
  export declare function readOcrSourceBytes(store: WorkspaceDatasetStore, input: {
113
80
  session: SessionLike;
package/lib/ocr/store.js CHANGED
@@ -1,23 +1,8 @@
1
1
  /**
2
- * `ocr` 的**产物层**:解析结果一律落 `capital-data/ocr/<doc_id>/`,模型侧只拿 `doc_id` 与索引。
3
- *
4
- * 为什么必须落盘而不是留在内存:一份 15 页研报归一化后约 2.7 万字符,而工具输出预算
5
- * 只有 6,144(`SOURCE_OUTPUT_BUDGET_CHARS`)。落盘之后"按页取回 / 关键词定位"才是**纯本地
6
- * 零花费**的第二、第三次读取;否则每次翻页都要重跑上游、再花一次 job。
7
- *
8
- * ```text
9
- * capital-data/ocr/<doc_id>/
10
- * ├── meta.json # 页偏移索引与花费口径(model / charts / job_id / created_at)
11
- * └── document.md # 全篇归一化 markdown,页分隔 `<!-- page:N -->`
12
- * ```
13
- *
14
- * `doc_id` 是**内容寻址**的:`sha1(归一化输入 + model + flags)` 前 12 位。因此"同一份文档
15
- * 重复解析"自动命中缓存不再出网;`refresh` 才强制重跑(上游对同一份 PDF 两次给出过
16
- * 18% 与 19% 的不同数字,重跑**不是**独立验证,引用必须带 `doc_id` 与 `created_at`)。
17
- *
18
- * 写入顺序是 `document.md` → `meta.json`:**meta 的在场即代表产物完整**。中途被中断
19
- * (超时、取消)留下半件产物时,下一次读取按"未缓存"处理并重写(`overwrite`),
20
- * 不会把模型永久锁死在一个坏 `doc_id` 上。
2
+ * OCR artifacts live under `capital-data/ocr/<doc_id>/`; the model only gets `doc_id`.
3
+ * Content-addressed ids make a repeat parse a cache hit. `document.md` is written before
4
+ * `meta.json`, so meta's presence means a complete artifact; half-written ones are redone.
5
+ * Page reads reuse the persisted text instead of re-running a paid upstream job.
21
6
  */
22
7
  import { DatasetStoreError } from '../data-collector/store.js';
23
8
  import { WORKSPACE_DATA_DIR } from '../data-collector/store.js';
@@ -25,12 +10,11 @@ export const OCR_ARTIFACT_SUBDIR = `${WORKSPACE_DATA_DIR}/ocr`;
25
10
  export const OCR_DOC_ID_PATTERN = /^ocr_[0-9a-f]{12}$/u;
26
11
  const META_FILE = 'meta.json';
27
12
  const DOCUMENT_FILE = 'document.md';
28
- /** 本地文档字节的读取上限:上游 100 页口径下的研报/公告量级,超限宁可报错也不要缓冲整份。 */
13
+ /** Read cap for local documents: 100-page reports fit; fail rather than buffer a whole file. */
29
14
  export const OCR_MAX_SOURCE_BYTES = 32 * 1024 * 1024;
30
15
  /**
31
- * 与 `hub.ts` / `sources.ts` 同一个 `process.getBuiltinModule` 惯用法(本仓刻意不装
32
- * `@types/node`)。拿不到就**响亮失败**:`doc_id` 是缓存键,退回一个更弱的哈希会让两份不同
33
- * 文档撞在同一个 `doc_id` 上、把 A 的正文当 B 交出去。
16
+ * No `@types/node` (same `process.getBuiltinModule` idiom as hub/sources). Fail loudly:
17
+ * `doc_id` is a cache key, so a weaker fallback could collide two documents onto one id.
34
18
  */
35
19
  function createHash(algorithm) {
36
20
  const processLike = globalThis.process;
@@ -39,14 +23,12 @@ function createHash(algorithm) {
39
23
  throw new Error('node:crypto is unavailable; cannot derive a content-addressed doc_id');
40
24
  return hash;
41
25
  }
42
- /** `seed` 由各形态自己拼(URL 文本 / 文件字节),统一在此收敛成 `ocr_<12 hex>`。 */
43
26
  export function deriveOcrDocId(seed, model, charts) {
44
27
  const hash = createHash('sha1');
45
28
  hash.update(seed);
46
29
  hash.update(`\u0000${model}\u0000${charts ? 'charts' : 'no-charts'}`);
47
30
  return `ocr_${hash.digest('hex').slice(0, 12)}`;
48
31
  }
49
- /** 回执里给模型的 opaque 引用(与 Dataset/profile 引用同一套 `workspace://` 口径)。 */
50
32
  export function ocrArtifactRef(docId) {
51
33
  return `workspace://${OCR_ARTIFACT_SUBDIR}/${docId}`;
52
34
  }
@@ -67,13 +49,7 @@ function isNotFound(error) {
67
49
  function isTooLarge(error) {
68
50
  return error instanceof DatasetStoreError && error.code === 'dataset_too_large';
69
51
  }
70
- /**
71
- * 落盘产物**自己**坏了(调用方没做错任何事)。
72
- *
73
- * 为什么要单独一个类型:翻成回执的那一层要把"下一步该传什么"写清楚,而那一步需要的指认
74
- * (当初解析用过的 url / file)只有这份 meta 里有。让调用方重新去猜链接,等于把一次
75
- * 可机械执行的修复推回给人或模型。
76
- */
52
+ /** Corrupt artifact, not caller error; the repair hint needs the source only this meta holds. */
77
53
  export class OcrArtifactError extends Error {
78
54
  reason;
79
55
  docId;
@@ -89,10 +65,8 @@ export class OcrArtifactError extends Error {
89
65
  }
90
66
  }
91
67
  /**
92
- * 读回一份产物。三种"读不到"必须能区分:
93
- * - `undefined`:从未解析过(meta 与 document 都不在)——调用方据此决定提交新 job。
94
- * - 抛错:meta 在但正文丢失/损坏,或半件产物——**不能**当成"没有内容",也不能静默重跑
95
- * (重跑要花一次 job,用户看不到钱去哪了),交给调用方翻成带 `doc_id` 的失败回执。
68
+ * `undefined` = never parsed (safe to submit a job). A throw = meta present but body
69
+ * missing/corrupt: do not treat as empty and do not silently re-run (a re-run costs a job).
96
70
  */
97
71
  export async function readOcrArtifact(store, input) {
98
72
  const docId = requireDocId(input.doc_id);
@@ -126,7 +100,7 @@ export async function readOcrArtifact(store, input) {
126
100
  }
127
101
  return { meta, document: { document: text, pages: meta.page_entries, chars: meta.chars, images: meta.images, tables: meta.tables, styleCharsRemoved: meta.style_chars_removed } };
128
102
  }
129
- /** 落盘。`overwrite` 只在调用方**已经决定**重新生成时传(见文件头的写入顺序)。 */
103
+ /** `overwrite` only when the caller has already decided to regenerate. */
130
104
  export async function writeOcrArtifact(store, input) {
131
105
  const docId = requireDocId(input.meta.doc_id);
132
106
  const written = await store.writeWorkspaceFiles({
@@ -141,7 +115,7 @@ export async function writeOcrArtifact(store, input) {
141
115
  });
142
116
  return { artifact_ref: ocrArtifactRef(docId), paths: written.map((file) => file.path) };
143
117
  }
144
- /** meta 是**我们**写出去的记录,读回来时仍逐字段核一遍——手改过的盘也要能响亮失败。 */
118
+ /** Re-validate our own record field by field so a hand-edited file fails loudly. */
145
119
  function normalizeMeta(value, docId) {
146
120
  const record = (typeof value === 'object' && value !== null ? value : {});
147
121
  const pages = Array.isArray(record.page_entries) ? record.page_entries : [];
@@ -183,13 +157,12 @@ function normalizeMeta(value, docId) {
183
157
  created_at: text('created_at'),
184
158
  };
185
159
  }
186
- /** 来源类别缺省或写歪时**响亮失败**:把不认识的 provenance 默认成 `url` 会伪造出处。 */
160
+ /** Fail on an unknown provenance: defaulting it to `url` would fabricate attribution. */
187
161
  function sourceKind(value) {
188
162
  if (value === 'url' || value === 'file' || value === 'job')
189
163
  return value;
190
164
  throw new Error(`meta.source_kind 不认识:${String(value)}`);
191
165
  }
192
- /** 拼一条与盘上记录同形状的 meta(`document` 只提供算好的排版统计)。 */
193
166
  export function buildArtifactMeta(input) {
194
167
  return {
195
168
  doc_id: requireDocId(input.doc_id),
@@ -208,11 +181,8 @@ export function buildArtifactMeta(input) {
208
181
  };
209
182
  }
210
183
  /**
211
- * 读取**待解析**的本地文档字节(`file` 形态的入口)。
212
- *
213
- * 只接受 session workspace 内的相对路径:绝对路径(含 Windows 盘符)与 `..` 段在
214
- * `normalizeWorkspaceRelativePath` 就被拒,之后仍要过沙箱归属校验。模型从 `@xxx.pdf`
215
- * mention 里看到的是路径末段,因此回执话术要求它传 workspace 相对路径。
184
+ * `file` entry: workspace-relative only (absolute paths, drive letters and `..` are
185
+ * rejected downstream, then ownership is checked); @mentions show just the path tail.
216
186
  */
217
187
  export async function readOcrSourceBytes(store, input) {
218
188
  const bytes = await store.readWorkspaceBytes({
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `tools/ocr` row: PaddleOCR job -> markdown -> disk. The `url` vs `file` / `doc_id`
3
+ * split is a call-level guard in `root-tool-policy` (name-level deny cannot express it).
4
+ * Client construction is per mount; credentials resolve per call.
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ export declare const name = "capital-ocr-tools";
8
+ export declare const inject: string[];
9
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,10 @@
1
+ import { registerOcrTool } from './web-retriever/tools.js';
2
+ import { createRetrieverRuntime } from './retriever-runtime.js';
3
+ import { capitalDatasetStore } from './runtime.js';
4
+ import { readRetrieverConfig } from './capital-settings.js';
5
+ export const name = 'capital-ocr-tools';
6
+ export const inject = ['tools'];
7
+ export function apply(ctx) {
8
+ const runtime = createRetrieverRuntime(ctx, readRetrieverConfig(ctx));
9
+ registerOcrTool(ctx, { store: capitalDatasetStore(ctx), client: runtime.ocrClient });
10
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Host-plane Capital policy row: call-level gates that preset composition cannot express.
3
+ * Root convergence (visibility already closed by family rows), per-name sibling deny,
4
+ * call-level guards for the `ocr` url form and role `interrupt_agent`, plus shell and fs
5
+ * guards. Identity must be read per call: the host creates the Agent before selecting a
6
+ * preset. Also hides delegation-only roles from the remote roster. Listens on `ctx.root`;
7
+ * one registration per process.
8
+ */
9
+ import type { Context } from '@deepseek-ai/cordis';
10
+ export declare const name = "capital-policy";
11
+ /**
12
+ * `tools` is a hard dependency: without `inject`, boot resolves it to undefined without
13
+ * throwing and every identity guard silently skips installation. Waiting for the fiber
14
+ * to provide it closes that boot-time race.
15
+ */
16
+ export declare const inject: string[];
17
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,22 @@
1
+ import { registerBashGuard } from './agents/bash-guard.js';
2
+ import { registerPresetRosterFilter } from './agents/preset-roster.js';
3
+ import { registerFsWriteGuard, registerRootOcrGuard, registerRootToolPolicy } from './agents/root-tool-policy.js';
4
+ import { registerRoleSiblingGuard, registerRoleToolPolicy } from './agents/role-tool-policy.js';
5
+ export const name = 'capital-policy';
6
+ /**
7
+ * `tools` is a hard dependency: without `inject`, boot resolves it to undefined without
8
+ * throwing and every identity guard silently skips installation. Waiting for the fiber
9
+ * to provide it closes that boot-time race.
10
+ */
11
+ export const inject = ['tools'];
12
+ export function apply(ctx) {
13
+ const policy = ctx;
14
+ // Preset rows now own visibility; the host keeps only call-level gates.
15
+ registerRootToolPolicy(policy, { deny: false });
16
+ registerRootOcrGuard(policy);
17
+ registerRoleToolPolicy(policy);
18
+ registerRoleSiblingGuard(policy);
19
+ registerBashGuard(policy);
20
+ registerFsWriteGuard(policy);
21
+ registerPresetRosterFilter(ctx);
22
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `tools/present` row: present_charts only. The root Agent's final-turn delivery step — the
3
+ * auto-mode chart pipeline returns refs upward and the main Agent redeems them here right before
4
+ * writing the closing answer (rationale: docs/dev/chart-delivery-events.md §6.3).
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ export declare const name = "capital-present-tools";
8
+ export declare const inject: string[];
9
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,13 @@
1
+ import { createChartEventPublisher } from './chart/events.js';
2
+ import { chartEventContextOf } from './chart/event-context.js';
3
+ import { registerPresentChartsTool } from './chart/present-tool.js';
4
+ import { capitalSharedRuntimeOf } from './chart-source-entry.js';
5
+ export const name = 'capital-present-tools';
6
+ export const inject = ['tools'];
7
+ export function apply(ctx) {
8
+ const shared = capitalSharedRuntimeOf(ctx);
9
+ registerPresentChartsTool(ctx, {
10
+ artifacts: shared.artifacts,
11
+ chartEvents: () => createChartEventPublisher(chartEventContextOf(ctx)),
12
+ });
13
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Single reader for an Agent's preset claim. Callers differ only in the fail direction:
3
+ * root convergence assumes a claim, role surfaces skip convergence, tool authorization
4
+ * falls back to the old topology check (which is never broader). Any exception is folded
5
+ * into flags, never thrown, because callers sit on the Agent creation path.
6
+ */
7
+ /** Registry surface unavailable, or `composedPreset` threw (see `failed`). */
8
+ export interface PresetClaimResult {
9
+ /** Raw `composedPreset` value; undefined means unbound Agent or a throw. */
10
+ readonly claim: string | undefined;
11
+ readonly registryMissing: boolean;
12
+ /** `composedPreset` threw; distinct from "no preset bound". */
13
+ readonly failed: boolean;
14
+ }
15
+ interface ReadableContext {
16
+ get?(name: string): unknown;
17
+ readonly [key: string]: unknown;
18
+ }
19
+ export declare function readPresetClaim(ctx: ReadableContext | undefined, agent: {
20
+ ctx?: unknown;
21
+ } | undefined): PresetClaimResult;
22
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Single reader for an Agent's preset claim. Callers differ only in the fail direction:
3
+ * root convergence assumes a claim, role surfaces skip convergence, tool authorization
4
+ * falls back to the old topology check (which is never broader). Any exception is folded
5
+ * into flags, never thrown, because callers sit on the Agent creation path.
6
+ */
7
+ export function readPresetClaim(ctx, agent) {
8
+ let presets;
9
+ try {
10
+ presets = ctx?.get?.('agentPresets');
11
+ }
12
+ catch {
13
+ return { claim: undefined, registryMissing: true, failed: false };
14
+ }
15
+ if (presets === undefined || typeof presets.composedPreset !== 'function') {
16
+ return { claim: undefined, registryMissing: true, failed: false };
17
+ }
18
+ try {
19
+ return { claim: presets.composedPreset(agent?.ctx), registryMissing: false, failed: false };
20
+ }
21
+ catch {
22
+ return { claim: undefined, registryMissing: false, failed: true };
23
+ }
24
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Single source of Capital preset identity. Since 0.2.1 the preset id is the role
3
+ * identity used by root convergence, shell guards, role surfaces, diagnostics and tests.
4
+ * These strings must match `preset/capital-generation/agent.patch.yml` `config.id` and
5
+ * delegation `agentPreset` values; `config.id` is also session-log identity, so do not
6
+ * rename casually. `test/persona.test.mjs` cross-checks them.
7
+ */
8
+ export declare const ROOT_PRESET_ID = "capital-generation";
9
+ export declare const ROLE_PRESET_IDS: {
10
+ readonly collector: "capital-data-collector";
11
+ readonly junior: "capital-data-junior";
12
+ readonly specialist: "capital-visualization-specialist";
13
+ readonly retriever: "capital-web-retriever";
14
+ readonly analyst: "capital-data-analyst";
15
+ };
16
+ export type RolePresetId = (typeof ROLE_PRESET_IDS)[keyof typeof ROLE_PRESET_IDS];
17
+ /** Role preset ids (array form avoids prototype-method coupling). */
18
+ export declare const ROLE_PRESET_ID_LIST: readonly string[];
19
+ /** Every preset claimed by this plugin: root plus five roles. */
20
+ export declare const CAPITAL_PRESET_IDS: readonly string[];
21
+ export declare function isCapitalPresetId(id: string | undefined): boolean;
22
+ /**
23
+ * Roles reached only through delegation. They stay registered because delegation rows resolve
24
+ * `config.id`, but the host roster filter hides them from the new-session selector.
25
+ */
26
+ export declare const INTERNAL_ROLE_PRESET_IDS: readonly string[];
27
+ export declare function isInternalRolePresetId(id: unknown): boolean;
28
+ /**
29
+ * Which preset may use which tool (enforced by `src/tool-exec.ts`). This replaces the old
30
+ * session-topology check, which rejected manually opened role sessions.
31
+ *
32
+ * The root preset must never appear in any set. These lists live at `src/` root because
33
+ * data and chart layers must not depend on the agents layer.
34
+ */
35
+ export declare const DATA_COLLECTION_PRESET_IDS: readonly string[];
36
+ export declare const DATASET_READING_PRESET_IDS: readonly string[];
37
+ /** May issue `chart_source_ref` for chart reads. */
38
+ export declare const CHART_SOURCE_PRESET_IDS: readonly string[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Single source of Capital preset identity. Since 0.2.1 the preset id is the role
3
+ * identity used by root convergence, shell guards, role surfaces, diagnostics and tests.
4
+ * These strings must match `preset/capital-generation/agent.patch.yml` `config.id` and
5
+ * delegation `agentPreset` values; `config.id` is also session-log identity, so do not
6
+ * rename casually. `test/persona.test.mjs` cross-checks them.
7
+ */
8
+ export const ROOT_PRESET_ID = 'capital-generation';
9
+ export const ROLE_PRESET_IDS = {
10
+ collector: 'capital-data-collector',
11
+ junior: 'capital-data-junior',
12
+ specialist: 'capital-visualization-specialist',
13
+ retriever: 'capital-web-retriever',
14
+ analyst: 'capital-data-analyst',
15
+ };
16
+ /** Role preset ids (array form avoids prototype-method coupling). */
17
+ export const ROLE_PRESET_ID_LIST = Object.values(ROLE_PRESET_IDS);
18
+ /** Every preset claimed by this plugin: root plus five roles. */
19
+ export const CAPITAL_PRESET_IDS = [ROOT_PRESET_ID, ...ROLE_PRESET_ID_LIST];
20
+ const CAPITAL_PRESET_ID_SET = new Set(CAPITAL_PRESET_IDS);
21
+ export function isCapitalPresetId(id) {
22
+ return id !== undefined && CAPITAL_PRESET_ID_SET.has(id);
23
+ }
24
+ /**
25
+ * Roles reached only through delegation. They stay registered because delegation rows resolve
26
+ * `config.id`, but the host roster filter hides them from the new-session selector.
27
+ */
28
+ export const INTERNAL_ROLE_PRESET_IDS = [
29
+ ROLE_PRESET_IDS.junior,
30
+ ROLE_PRESET_IDS.specialist,
31
+ ];
32
+ const INTERNAL_ROLE_PRESET_ID_SET = new Set(INTERNAL_ROLE_PRESET_IDS);
33
+ export function isInternalRolePresetId(id) {
34
+ return typeof id === 'string' && INTERNAL_ROLE_PRESET_ID_SET.has(id);
35
+ }
36
+ /**
37
+ * Which preset may use which tool (enforced by `src/tool-exec.ts`). This replaces the old
38
+ * session-topology check, which rejected manually opened role sessions.
39
+ *
40
+ * The root preset must never appear in any set. These lists live at `src/` root because
41
+ * data and chart layers must not depend on the agents layer.
42
+ */
43
+ export const DATA_COLLECTION_PRESET_IDS = [ROLE_PRESET_IDS.collector];
44
+ export const DATASET_READING_PRESET_IDS = [ROLE_PRESET_IDS.junior, ROLE_PRESET_IDS.analyst];
45
+ /** May issue `chart_source_ref` for chart reads. */
46
+ export const CHART_SOURCE_PRESET_IDS = [ROLE_PRESET_IDS.junior];