@v587d/capital-generation 2.8.3 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -1
- package/README.md +40 -19
- package/chart-ui/README.md +1 -1
- package/chart-ui/client.src.cjs +7 -16
- package/chart-ui/index.js +12 -33
- package/chart-ui/package.json +2 -2
- package/config/client.js +18 -39
- package/config/client.src.cjs +79 -217
- package/config/index.js +22 -72
- package/config/package.json +2 -2
- package/cordis.patch.yml +46 -51
- package/lib/agents/bash-guard.d.ts +9 -55
- package/lib/agents/bash-guard.js +27 -67
- package/lib/agents/preset-roster.d.ts +22 -0
- package/lib/agents/preset-roster.js +87 -0
- package/lib/agents/role-tool-policy.d.ts +46 -0
- package/lib/agents/role-tool-policy.js +205 -0
- package/lib/agents/root-tool-policy.d.ts +28 -124
- package/lib/agents/root-tool-policy.js +164 -237
- package/lib/agents/selected-skills-provider.d.ts +2 -6
- package/lib/agents/selected-skills-provider.js +27 -84
- package/lib/capital-settings.d.ts +27 -0
- package/lib/capital-settings.js +23 -0
- package/lib/chart/artifact-ref.d.ts +3 -8
- package/lib/chart/artifact-ref.js +3 -8
- package/lib/chart/downsample.d.ts +2 -5
- package/lib/chart/downsample.js +3 -8
- package/lib/chart/errors.d.ts +3 -7
- package/lib/chart/errors.js +1 -2
- package/lib/chart/event-context.d.ts +8 -0
- package/lib/chart/event-context.js +39 -0
- package/lib/chart/events.d.ts +21 -78
- package/lib/chart/events.js +40 -189
- package/lib/chart/html.d.ts +2 -5
- package/lib/chart/html.js +8 -15
- package/lib/chart/runtime.d.ts +5 -18
- package/lib/chart/runtime.js +14 -34
- package/lib/chart/series.d.ts +10 -17
- package/lib/chart/series.js +22 -47
- package/lib/chart/source-token.d.ts +5 -13
- package/lib/chart/source-token.js +18 -17
- package/lib/chart/spec.d.ts +6 -12
- package/lib/chart/spec.js +7 -31
- package/lib/chart/tool.d.ts +6 -29
- package/lib/chart/tool.js +9 -23
- package/lib/chart-render-entry.d.ts +9 -0
- package/lib/chart-render-entry.js +17 -0
- package/lib/chart-source-entry.d.ts +13 -0
- package/lib/chart-source-entry.js +20 -0
- package/lib/collect-entry.d.ts +9 -0
- package/lib/collect-entry.js +10 -0
- package/lib/collector-runtime.d.ts +22 -0
- package/lib/collector-runtime.js +97 -0
- package/lib/data-collector/args.d.ts +6 -24
- package/lib/data-collector/args.js +6 -24
- package/lib/data-collector/dataset-tools.d.ts +7 -1
- package/lib/data-collector/dataset-tools.js +27 -56
- package/lib/data-collector/describe.d.ts +8 -33
- package/lib/data-collector/describe.js +17 -70
- package/lib/data-collector/hub.d.ts +27 -85
- package/lib/data-collector/hub.js +19 -85
- package/lib/data-collector/query-time.d.ts +7 -26
- package/lib/data-collector/query-time.js +8 -27
- package/lib/data-collector/store.d.ts +40 -117
- package/lib/data-collector/store.js +32 -111
- package/lib/data-collector/time-axis.d.ts +16 -76
- package/lib/data-collector/time-axis.js +25 -111
- package/lib/data-collector/tools.d.ts +6 -6
- package/lib/data-collector/tools.js +34 -43
- package/lib/dataset-read-entry.d.ts +9 -0
- package/lib/dataset-read-entry.js +7 -0
- package/lib/index.d.ts +7 -117
- package/lib/index.js +43 -422
- package/lib/net/eastmoney-client.d.ts +13 -65
- package/lib/net/eastmoney-client.js +12 -53
- package/lib/net/throttle.d.ts +2 -13
- package/lib/net/throttle.js +3 -14
- package/lib/ocr/client.d.ts +12 -21
- package/lib/ocr/client.js +23 -40
- package/lib/ocr/markdown.d.ts +20 -26
- package/lib/ocr/markdown.js +23 -36
- package/lib/ocr/store.d.ts +14 -47
- package/lib/ocr/store.js +15 -45
- package/lib/ocr-entry.d.ts +9 -0
- package/lib/ocr-entry.js +10 -0
- package/lib/policy-entry.d.ts +17 -0
- package/lib/policy-entry.js +22 -0
- package/lib/preset-claim.d.ts +22 -0
- package/lib/preset-claim.js +24 -0
- package/lib/preset-ids.d.ts +38 -0
- package/lib/preset-ids.js +46 -0
- package/lib/retriever-config.d.ts +37 -0
- package/lib/retriever-config.js +36 -0
- package/lib/retriever-runtime.d.ts +31 -0
- package/lib/retriever-runtime.js +82 -0
- package/lib/root-extras-entry.d.ts +17 -0
- package/lib/root-extras-entry.js +25 -0
- package/lib/runtime-entry.d.ts +15 -0
- package/lib/runtime-entry.js +5 -0
- package/lib/runtime.d.ts +21 -0
- package/lib/runtime.js +33 -0
- package/lib/search-entry.d.ts +9 -0
- package/lib/search-entry.js +9 -0
- package/lib/sources/eastmoney-http.js +20 -68
- package/lib/sources/ftshare-rest.d.ts +6 -34
- package/lib/sources/ftshare-rest.js +84 -292
- package/lib/sources/fuyao-core.d.ts +8 -18
- package/lib/sources/fuyao-core.js +10 -21
- package/lib/sources/fuyao-rest.d.ts +2 -6
- package/lib/sources/fuyao-rest.js +23 -102
- package/lib/sources/security-code.d.ts +6 -12
- package/lib/sources/security-code.js +6 -12
- package/lib/sources/security-identity.d.ts +1 -4
- package/lib/sources/security-identity.js +3 -8
- package/lib/sources/tencent-http.js +14 -28
- package/lib/sources/tencent-public-core.d.ts +8 -60
- package/lib/sources/tencent-public-core.js +12 -77
- package/lib/sources/wind-mcp.d.ts +0 -5
- package/lib/sources/wind-mcp.js +23 -78
- package/lib/sources-entry.d.ts +9 -0
- package/lib/sources-entry.js +9 -0
- package/lib/time/tools.d.ts +8 -31
- package/lib/time/tools.js +30 -66
- package/lib/time-entry.d.ts +9 -0
- package/lib/time-entry.js +7 -0
- package/lib/tool-exec.d.ts +45 -11
- package/lib/tool-exec.js +59 -6
- package/lib/user-customization.d.ts +14 -0
- package/lib/user-customization.js +47 -0
- package/lib/watchlist/tools.d.ts +3 -13
- package/lib/watchlist/tools.js +10 -30
- package/lib/watchlist-entry.d.ts +9 -0
- package/lib/watchlist-entry.js +6 -0
- package/lib/web-retriever/engines.d.ts +6 -11
- package/lib/web-retriever/engines.js +9 -14
- package/lib/web-retriever/html-markdown.d.ts +14 -34
- package/lib/web-retriever/html-markdown.js +39 -75
- package/lib/web-retriever/local-fetch.d.ts +22 -58
- package/lib/web-retriever/local-fetch.js +33 -77
- package/lib/web-retriever/retriever.d.ts +6 -18
- package/lib/web-retriever/retriever.js +7 -7
- package/lib/web-retriever/sources.d.ts +37 -124
- package/lib/web-retriever/sources.js +64 -196
- package/lib/web-retriever/tools.d.ts +43 -27
- package/lib/web-retriever/tools.js +99 -137
- package/lib/web-retriever/wind-client.d.ts +14 -31
- package/lib/web-retriever/wind-client.js +16 -32
- package/locale/zh.json +1 -1
- package/package.json +67 -7
- package/preset/capital-generation/README.md +87 -53
- package/preset/capital-generation/agent.patch.yml +526 -337
- package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
- package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
- package/watchlist/client.js +15 -148
- package/watchlist/client.src.cjs +148 -662
- package/watchlist/holding-rules.js +11 -38
- package/watchlist/index.js +127 -397
- package/watchlist/package.json +2 -2
|
@@ -2,49 +2,65 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
2
2
|
import { WebRetriever } from './retriever.js';
|
|
3
3
|
import type { WindClient } from './wind-client.js';
|
|
4
4
|
import type { LocalFetchOptions } from './local-fetch.js';
|
|
5
|
+
import { type AgentExecutionLike } from '../tool-exec.js';
|
|
5
6
|
import { type WorkspaceDatasetStore } from '../data-collector/store.js';
|
|
6
7
|
import { type PaddleOcrClient } from '../ocr/client.js';
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
* Use the framework's exec shape (`src/tool-exec.ts`): `ocr` needs the caller
|
|
10
|
+
* session; a self-made shape would bypass the real `header.cwd` path.
|
|
11
|
+
*/
|
|
12
|
+
type ToolExecLike = AgentExecutionLike;
|
|
13
|
+
interface RetrievalTrace {
|
|
14
|
+
provider: string;
|
|
15
|
+
tool: string;
|
|
16
|
+
query: string;
|
|
17
|
+
at: number;
|
|
18
|
+
via?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Per-caller-session, in-memory: capped recent retrievals plus per-provider tally.
|
|
22
|
+
* Advisory only (never blocks); retriever/client stay stateless.
|
|
23
|
+
*/
|
|
24
|
+
interface RetrievalEcho {
|
|
25
|
+
recent: RetrievalTrace[];
|
|
26
|
+
tally: Record<string, number>;
|
|
27
|
+
/** The stored trace just written, so callers can backfill `via`. */
|
|
28
|
+
entry: RetrievalTrace;
|
|
29
|
+
/** Fresh snapshot after backfilling `entry.via`. */
|
|
30
|
+
snapshot: () => RetrievalTrace[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Search family: anysearch search/fetch plus the two wind_docs tools. `echo` is
|
|
34
|
+
* per registration (one family row), so its receipts cover only this family.
|
|
35
|
+
*/
|
|
36
|
+
export declare function registerSearchTools(ctx: Context, retriever: WebRetriever, windClient: WindClient, echo?: (exec: ToolExecLike, entry: RetrievalTrace) => RetrievalEcho): void;
|
|
37
|
+
/**
|
|
38
|
+
* Legacy single-bundle entry: search, wind and ocr tools share one echo, matching
|
|
39
|
+
* the old monolith behavior.
|
|
15
40
|
*/
|
|
16
41
|
export declare function registerWebRetrieverTools(ctx: Context, retriever: WebRetriever, windClient: WindClient, ocr: OcrToolDependencies): void;
|
|
17
|
-
/** `ocr`
|
|
42
|
+
/** Register only `ocr` (root keeps local document parsing, no search/source tools). */
|
|
43
|
+
export declare function registerOcrTool(ctx: Context, ocr: OcrToolDependencies, echo?: (exec: ToolExecLike, entry: RetrievalTrace) => RetrievalEcho): void;
|
|
44
|
+
/** `ocr` deps: upstream client + workspace store (shared with Dataset). */
|
|
18
45
|
export interface OcrToolDependencies {
|
|
19
46
|
store: WorkspaceDatasetStore;
|
|
20
47
|
client: PaddleOcrClient;
|
|
21
|
-
/**
|
|
48
|
+
/** Injectable for tests pinning `created_at`. */
|
|
22
49
|
now?: () => number;
|
|
23
|
-
/**
|
|
24
|
-
* 注入以便测试:`url` 形态的公网性闸门默认走**真实 DNS**(与本机直连同一份实现,§9.7),
|
|
25
|
-
* 生产路径不传。生产代码里没有任何调用方给它赋值——它只是把 DNS 这一步挪出断言范围。
|
|
26
|
-
*/
|
|
50
|
+
/** Test-only DNS override; production never sets it. */
|
|
27
51
|
resolveAddresses?: LocalFetchOptions['resolveAddresses'];
|
|
28
52
|
}
|
|
29
53
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* (由 `test/persona.test.mjs` 钉住与本常量同源)。超过阈值的工具结果在会话历史里会被
|
|
33
|
-
* `dsh-compaction-tool-result-pruner` 换成 head 4096 + `[... middle pruned ...]` + tail 1024,
|
|
34
|
-
* 也就是**中间条目被无声吃掉**,而留在头部的 `count` 还写着原来的条数。
|
|
35
|
-
* 留 25% 余量的做法与数据面能力目录的预算同一口径(`test/data-collector-hub.test.mjs`)。
|
|
54
|
+
* Source output budget = 75% of the host's 8192 tool-result pruning threshold.
|
|
55
|
+
* Over that, the middle is silently pruned while `count` still claims the full total.
|
|
36
56
|
*/
|
|
37
57
|
export declare const SOURCE_OUTPUT_BUDGET_CHARS = 6144;
|
|
38
58
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* 不依赖 AnySearch / Wind;任何一路的配置缺失都不应连带影响另外几路的注册。
|
|
43
|
-
*
|
|
44
|
-
* `enabled`(设置卡片「允许启动本地提取网页内容」)**同时支配这九个工具的执行**:
|
|
45
|
-
* 关闭时工具仍在注册面(不静默消失),但每次调用响亮失败并说明开关位置——
|
|
46
|
-
* 卡片文案的字面语义对模型与用户都成立,用户同意不被旁路。
|
|
59
|
+
* Nine source tools depend only on HTTP egress, so they register independently of
|
|
60
|
+
* AnySearch/Wind config. `enabled=false` still registers but each call fails loudly
|
|
61
|
+
* naming the settings switch, so the switch semantics hold for user and model.
|
|
47
62
|
*/
|
|
48
63
|
export declare function registerSourceTools(ctx: Context, localFetchOptions: LocalFetchOptions & {
|
|
49
64
|
enabled?: boolean;
|
|
50
65
|
}): void;
|
|
66
|
+
export {};
|
|
@@ -98,24 +98,9 @@ function fetchOutput(result, recent, tally) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* `createSuccessResult` 硬编码 `isError: false`——**返回值没有任何办法标记失败**,
|
|
105
|
-
* 唯一通道是从 `execute` 抛出(`dispatchToolBody` 的 catch → `toolErrorResult`,
|
|
106
|
-
* 产出 `{ text: "Error: …", isError: true, error: { message } }`)。官方
|
|
107
|
-
* `web_fetch` / `web_search` 就是抛 `WebError`。
|
|
108
|
-
*
|
|
109
|
-
* 2026-09 实测缺陷:本工具此前把失败做成 `ok:false` 信封**正常返回**,于是子 Agent
|
|
110
|
-
* 会话日志里 7 次实际失败的抓取全部记成 `isError: false`,UI 与模型都当成成功,
|
|
111
|
-
* 任何基于 `isError` 的重试/统计/压缩逻辑也一并失效。
|
|
112
|
-
*
|
|
113
|
-
* 抛出的消息带**完整结构化回执**,所以 `via` / `fallback` / `local_error` /
|
|
114
|
-
* `recent_retrievals` / `provider_tally` 与来源标注协议字段一个都不丢。
|
|
115
|
-
*
|
|
116
|
-
* 范围:只覆盖 `web_retriever_fetch`(本地回退所在工具)。`anysearch_search`
|
|
117
|
-
* 与两个 `wind_docs_*` 工具的错误信号是同一类问题,但按 2026-09 决定**不在本次改动**
|
|
118
|
-
* 内,审计与后续安排见 `docs/design/tool-result-error-signal.md`。
|
|
101
|
+
* Tool failures must be thrown: DSH hardcodes `isError: false` on success results,
|
|
102
|
+
* so a returned `ok:false` envelope is logged as success. The thrown message carries
|
|
103
|
+
* the full structured receipt so via/fallback/local_error survive.
|
|
119
104
|
*/
|
|
120
105
|
function throwToolFailure(payload) {
|
|
121
106
|
throw new Error(JSON.stringify(payload));
|
|
@@ -139,19 +124,13 @@ const WIND_NEWS_TOOL = 'wind_docs_news';
|
|
|
139
124
|
const WIND_ANNOUNCEMENTS_UPSTREAM = 'get_company_announcements';
|
|
140
125
|
const WIND_NEWS_UPSTREAM = 'get_financial_news';
|
|
141
126
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* 不可用只影响调用结果(错误信封),不影响工具注册与子 Agent 创建。
|
|
145
|
-
*
|
|
146
|
-
* `ocr` 与 `datasetStore` 共用 workspace 落盘,而检索回声(`recent_retrievals` /
|
|
147
|
-
* `provider_tally`)**必须**与另外四个工具共享同一份闭包,所以它在这同一个 `definitions`
|
|
148
|
-
* 数组里注册,不另起一个注册函数。
|
|
127
|
+
* Search family: anysearch search/fetch plus the two wind_docs tools. `echo` is
|
|
128
|
+
* per registration (one family row), so its receipts cover only this family.
|
|
149
129
|
*/
|
|
150
|
-
export function
|
|
130
|
+
export function registerSearchTools(ctx, retriever, windClient, echo = createRetrievalEcho()) {
|
|
151
131
|
const tools = ctx.get('tools');
|
|
152
132
|
if (!tools)
|
|
153
133
|
return;
|
|
154
|
-
const echo = createRetrievalEcho();
|
|
155
134
|
const trace = (exec, provider, tool, query) => echo(exec, { provider, tool, query, at: Date.now() });
|
|
156
135
|
const definitions = [
|
|
157
136
|
{
|
|
@@ -182,7 +161,7 @@ export function registerWebRetrieverTools(ctx, retriever, windClient, ocr) {
|
|
|
182
161
|
const result = await retriever.fetch(url, exec.signal);
|
|
183
162
|
entry.via = result.via;
|
|
184
163
|
const payload = fetchOutput(result, snapshot(), tally);
|
|
185
|
-
//
|
|
164
|
+
// A failure must throw (isError: true); a successful-looking error envelope is not.
|
|
186
165
|
if (result.ok === false)
|
|
187
166
|
throwToolFailure(payload);
|
|
188
167
|
return payload;
|
|
@@ -220,40 +199,58 @@ export function registerWebRetrieverTools(ctx, retriever, windClient, ocr) {
|
|
|
220
199
|
return windOutput(WIND_NEWS_TOOL, query, result, recent, tally);
|
|
221
200
|
},
|
|
222
201
|
},
|
|
223
|
-
{
|
|
224
|
-
name: 'ocr',
|
|
225
|
-
timeoutMs: OCR_TOOL_TIMEOUT_MS,
|
|
226
|
-
description: '把一份 PDF 或图片解析成模型可读的 markdown(PaddleOCR 文档解析,需要 Token)。一次调用只走一种形态:url=公网 .pdf 直链(本机不抓文件,由上游取);file=session workspace 内的相对路径(pdf/png/jpg/jpeg/webp/bmp/tiff,@mention 带来的开头 @ 可省);job_id + doc_id=继续等上一次 status:pending 回执里的作业(**不重复提交、不再花钱**);doc_id=读已落盘的结果,可配 pages("1-3" / "5" / "2-4,9",一次最多 8 页)或 query(关键词定位,回命中片段与页号)。⚠️ 作业**整篇一次算完**:pages 只影响读、不影响算价,也不会更便宜;同一份文档重复调用会命中 doc_id 缓存、不再出网。结果一律落 capital-data/ocr/<doc_id>/,回执在预算内给全文、超预算只给页索引与第 1 页。没有 Token、解析失败或文档超页时须如实说明边界:不得把标题当正文引用,不得把 PDF 链接当正文来源标注(正文来自解析结果,引用写 doc_id 与页号)。',
|
|
227
|
-
parameters: jsonObject({
|
|
228
|
-
url: { type: 'string', description: '公网可达的 .pdf 直链(http/https,不含凭据,不接受内网地址与 IP 字面量)' },
|
|
229
|
-
file: { type: 'string', description: 'session workspace 内的相对路径,例如 refs/贵州茅台研报.pdf' },
|
|
230
|
-
job_id: { type: 'string', description: '上一次回执 status:pending 里的 job_id;必须与同一回执的 doc_id 一起传' },
|
|
231
|
-
doc_id: { type: 'string', description: '解析结果标识(形如 ocr_1a2b3c4d5e6f),来自回执' },
|
|
232
|
-
pages: { type: 'string', description: '配合 doc_id 读取的页区间,1 起始闭区间,例如 "2-4,9";一次最多 8 页' },
|
|
233
|
-
query: { type: 'string', description: `配合 doc_id 的关键词定位,最多 ${OCR_MAX_QUERY_CHARS} 字符` },
|
|
234
|
-
limit: { type: 'integer', description: `query 命中数上限 1~${OCR_MAX_QUERY_LIMIT},默认 ${OCR_DEFAULT_QUERY_LIMIT}` },
|
|
235
|
-
charts: { type: 'boolean', description: '可选,是否启用图表识别(默认 true);只影响 url/file 形态的这一次提交' },
|
|
236
|
-
refresh: { type: 'boolean', description: '可选,忽略缓存重新解析(默认 false);只接受与 url/file 同时传,重跑会再花一次 job' },
|
|
237
|
-
}, []),
|
|
238
|
-
output: { schema: { type: 'object', additionalProperties: true }, render },
|
|
239
|
-
async execute(args, exec) {
|
|
240
|
-
return runOcr(ocr, trace, args, exec);
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
202
|
];
|
|
244
203
|
for (const definition of definitions) {
|
|
245
204
|
ctx.effect(() => tools.register(definition), `capital-generation.tool(${definition.name})`);
|
|
246
205
|
}
|
|
247
206
|
}
|
|
248
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Legacy single-bundle entry: search, wind and ocr tools share one echo, matching
|
|
209
|
+
* the old monolith behavior.
|
|
210
|
+
*/
|
|
211
|
+
export function registerWebRetrieverTools(ctx, retriever, windClient, ocr) {
|
|
212
|
+
const echo = createRetrievalEcho();
|
|
213
|
+
registerSearchTools(ctx, retriever, windClient, echo);
|
|
214
|
+
registerOcrTool(ctx, ocr, echo);
|
|
215
|
+
}
|
|
216
|
+
/** `ocr` tool definition shared by the retrieve entry and the root local-only entry. */
|
|
217
|
+
function ocrToolDefinition(ocr, trace) {
|
|
218
|
+
return {
|
|
219
|
+
name: 'ocr',
|
|
220
|
+
timeoutMs: OCR_TOOL_TIMEOUT_MS,
|
|
221
|
+
description: '把一份 PDF 或图片解析成模型可读的 markdown(PaddleOCR 文档解析,需要 Token)。一次调用只走一种形态:url=公网 .pdf 直链(本机不抓文件,由上游取);file=session workspace 内的相对路径(pdf/png/jpg/jpeg/webp/bmp/tiff,@mention 带来的开头 @ 可省);job_id + doc_id=继续等上一次 status:pending 回执里的作业(**不重复提交、不再花钱**);doc_id=读已落盘的结果,可配 pages("1-3" / "5" / "2-4,9",一次最多 8 页)或 query(关键词定位,回命中片段与页号)。⚠️ 作业**整篇一次算完**:pages 只影响读、不影响算价,也不会更便宜;同一份文档重复调用会命中 doc_id 缓存、不再出网。结果一律落 capital-data/ocr/<doc_id>/,回执在预算内给全文、超预算只给页索引与第 1 页。没有 Token、解析失败或文档超页时须如实说明边界:不得把标题当正文引用,不得把 PDF 链接当正文来源标注(正文来自解析结果,引用写 doc_id 与页号)。',
|
|
222
|
+
parameters: jsonObject({
|
|
223
|
+
url: { type: 'string', description: '公网可达的 .pdf 直链(http/https,不含凭据,不接受内网地址与 IP 字面量)' },
|
|
224
|
+
file: { type: 'string', description: 'session workspace 内的相对路径,例如 refs/贵州茅台研报.pdf' },
|
|
225
|
+
job_id: { type: 'string', description: '上一次回执 status:pending 里的 job_id;必须与同一回执的 doc_id 一起传' },
|
|
226
|
+
doc_id: { type: 'string', description: '解析结果标识(形如 ocr_1a2b3c4d5e6f),来自回执' },
|
|
227
|
+
pages: { type: 'string', description: '配合 doc_id 读取的页区间,1 起始闭区间,例如 "2-4,9";一次最多 8 页' },
|
|
228
|
+
query: { type: 'string', description: `配合 doc_id 的关键词定位,最多 ${OCR_MAX_QUERY_CHARS} 字符` },
|
|
229
|
+
limit: { type: 'integer', description: `query 命中数上限 1~${OCR_MAX_QUERY_LIMIT},默认 ${OCR_DEFAULT_QUERY_LIMIT}` },
|
|
230
|
+
charts: { type: 'boolean', description: '可选,是否启用图表识别(默认 true);只影响 url/file 形态的这一次提交' },
|
|
231
|
+
refresh: { type: 'boolean', description: '可选,忽略缓存重新解析(默认 false);只接受与 url/file 同时传,重跑会再花一次 job' },
|
|
232
|
+
}, []),
|
|
233
|
+
output: { schema: { type: 'object', additionalProperties: true }, render },
|
|
234
|
+
async execute(args, exec) {
|
|
235
|
+
return runOcr(ocr, trace, args, exec);
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Register only `ocr` (root keeps local document parsing, no search/source tools). */
|
|
240
|
+
export function registerOcrTool(ctx, ocr, echo = createRetrievalEcho()) {
|
|
241
|
+
const tools = ctx.get('tools');
|
|
242
|
+
if (!tools)
|
|
243
|
+
return;
|
|
244
|
+
const trace = (exec, provider, tool, query) => echo(exec, { provider, tool, query, at: Date.now() });
|
|
245
|
+
const definition = ocrToolDefinition(ocr, trace);
|
|
246
|
+
ctx.effect(() => tools.register(definition), 'capital-generation.tool(ocr)');
|
|
247
|
+
}
|
|
248
|
+
// Document parsing: ocr (PDF / image -> markdown, always persisted)
|
|
249
249
|
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
// (谁的 `.pdf` 直链都能喂进来),也**不受 `localFetch.enabled` 支配**:那道开关的字面
|
|
253
|
-
// 语义是"允许本机直连提取网页内容",而这里的文件由上游解析服务取,用户同意的是另一件事
|
|
254
|
-
// (Token 卡片)。判据写在 `docs/dev/web-retriever.md` §4.3。
|
|
250
|
+
// This is the parse workface: not bound to a source, and not gated by
|
|
251
|
+
// `localFetch.enabled` (upstream fetches the file; user consent is the token card).
|
|
255
252
|
const OCR_TOOL_TIMEOUT_MS = 240_000;
|
|
256
|
-
/**
|
|
253
|
+
/** Read-back cap per `pages` call; limits receipt size, not compute (whole doc was parsed). */
|
|
257
254
|
const OCR_MAX_PAGES_PER_READ = 8;
|
|
258
255
|
const OCR_QUERY_WINDOW_CHARS = 320;
|
|
259
256
|
const OCR_MAX_QUERY_CHARS = 64;
|
|
@@ -261,7 +258,7 @@ const OCR_DEFAULT_QUERY_LIMIT = 5;
|
|
|
261
258
|
const OCR_MAX_QUERY_LIMIT = 20;
|
|
262
259
|
const OCR_LOCAL_SUFFIXES = ['pdf', 'png', 'jpg', 'jpeg', 'webp', 'bmp', 'tiff'];
|
|
263
260
|
const PDF_URL_SUFFIX = /\.pdf$/iu;
|
|
264
|
-
/**
|
|
261
|
+
/** Tool-side failure (argument shape, missing cache) with an actionable hint. */
|
|
265
262
|
class OcrToolError extends Error {
|
|
266
263
|
code;
|
|
267
264
|
hint;
|
|
@@ -280,10 +277,8 @@ function errorMessage(error) {
|
|
|
280
277
|
return error instanceof Error ? error.message : String(error);
|
|
281
278
|
}
|
|
282
279
|
/**
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* 指认来自 meta,不来自调用方——`doc_id` 是内容寻址的,用同一个 source 重跑必然落回同一个
|
|
286
|
-
* `doc_id`,所以这条 hint 是无脑可执行的。认不出 source 形态时只说"重新解析一次",不编参数。
|
|
280
|
+
* Build the repair hint from meta: doc_id is content-addressed, so replaying the
|
|
281
|
+
* stored source lands on the same artifact. Unknown source => generic advice.
|
|
287
282
|
*/
|
|
288
283
|
function artifactRepair(sourceKind, source) {
|
|
289
284
|
const prefix = sourceKind === 'url' ? 'url:' : sourceKind === 'file' ? 'file:' : undefined;
|
|
@@ -293,11 +288,7 @@ function artifactRepair(sourceKind, source) {
|
|
|
293
288
|
const argument = JSON.stringify(source.slice(prefix.length));
|
|
294
289
|
return `再调一次 { ${prefix.slice(0, -1)}: ${argument}, refresh: true } 重新解析(doc_id 由内容寻址,重跑落回同一个产物)`;
|
|
295
290
|
}
|
|
296
|
-
/**
|
|
297
|
-
* 三条实现各自的异常(`OcrToolError` / `OcrError` / 出口闸门 / `DatasetStoreError`)
|
|
298
|
-
* 翻成**一套**回执码。认不出的仍退回 `INVALID_INPUT` 并照抄消息——绝不假装知道上游
|
|
299
|
-
* 或宿主失败的原因。
|
|
300
|
-
*/
|
|
291
|
+
/** Map all implementation errors to one receipt code set; unknown => INVALID_INPUT. */
|
|
301
292
|
function ocrFailure(error) {
|
|
302
293
|
if (error instanceof OcrToolError) {
|
|
303
294
|
return { code: error.code, error: error.message, ...(error.hint ? { hint: error.hint } : {}) };
|
|
@@ -305,8 +296,7 @@ function ocrFailure(error) {
|
|
|
305
296
|
if (error instanceof OcrError)
|
|
306
297
|
return { code: error.code, error: error.message };
|
|
307
298
|
if (error instanceof OcrArtifactError) {
|
|
308
|
-
//
|
|
309
|
-
// 而正确的下一步只有一条——拿 meta 里的 source 重跑那一次解析。
|
|
299
|
+
// A corrupt artifact is not caller error; the only fix is replaying meta.source.
|
|
310
300
|
return { code: 'RESULT_INVALID', error: error.message, hint: artifactRepair(error.sourceKind, error.source) };
|
|
311
301
|
}
|
|
312
302
|
const code = error?.code;
|
|
@@ -359,10 +349,8 @@ function requireDocId(value) {
|
|
|
359
349
|
return docId;
|
|
360
350
|
}
|
|
361
351
|
/**
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* 歧义一律响亮拒绝而不是"挑一个":`url` + `file` 同时给、`pages` 配 `query`、`refresh`
|
|
365
|
-
* 配 `doc_id`——静默选边会让模型以为自己要的东西还在。
|
|
352
|
+
* Normalize the four input forms here. Ambiguous combinations fail loudly instead
|
|
353
|
+
* of silently picking one.
|
|
366
354
|
*/
|
|
367
355
|
function ocrRequest(args) {
|
|
368
356
|
const present = (key) => args[key] !== undefined && args[key] !== null && args[key] !== '';
|
|
@@ -412,7 +400,7 @@ function ocrRequest(args) {
|
|
|
412
400
|
const file = requiredString(args.file, 'file', 512);
|
|
413
401
|
return { mode: 'file', label: file, file, charts, refresh };
|
|
414
402
|
}
|
|
415
|
-
/**
|
|
403
|
+
/** Strip a leading `@` from @mention; suffix allowlist only (ownership is checked in store). */
|
|
416
404
|
function normalizeLocalPath(value) {
|
|
417
405
|
const raw = value.startsWith('@') ? value.slice(1).trim() : value.trim();
|
|
418
406
|
const suffix = (raw.includes('.') ? raw.slice(raw.lastIndexOf('.') + 1) : '').toLowerCase();
|
|
@@ -454,7 +442,7 @@ function metaFields(meta, cached) {
|
|
|
454
442
|
: {}),
|
|
455
443
|
};
|
|
456
444
|
}
|
|
457
|
-
/**
|
|
445
|
+
/** Quantify cleanup: proves the shrink is style removal, not truncation. */
|
|
458
446
|
function cleanupNote(meta) {
|
|
459
447
|
if (meta.style_chars_removed === 0 && meta.tables === 0)
|
|
460
448
|
return undefined;
|
|
@@ -465,10 +453,7 @@ function cleanupNote(meta) {
|
|
|
465
453
|
parts.push(`${meta.tables} 张 HTML 表已转成 markdown 表格`);
|
|
466
454
|
return parts.join(';');
|
|
467
455
|
}
|
|
468
|
-
/**
|
|
469
|
-
* `done` 回执的装载:整篇放得下就给全文,否则给页索引 + 第 1 页;连索引都放不下就只给
|
|
470
|
-
* 边界说明。**永不半路截断**——截断的正文会被当成完整正文引用,比少给一页危险。
|
|
471
|
-
*/
|
|
456
|
+
/** Whole text if it fits, else page index + page 1; never truncate mid-page. */
|
|
472
457
|
function doneReceipt(artifact, input) {
|
|
473
458
|
const { meta, document } = artifact;
|
|
474
459
|
const cleanup = cleanupNote(meta);
|
|
@@ -486,7 +471,7 @@ function doneReceipt(artifact, input) {
|
|
|
486
471
|
? undefined
|
|
487
472
|
: { page: first.page, chars: first.chars, ...(first.heading === undefined ? {} : { heading: first.heading }), text: document.document.slice(first.start, first.end) };
|
|
488
473
|
const withIndex = { ...base, index, note: budgetNote(meta, cleanup, false) };
|
|
489
|
-
//
|
|
474
|
+
// Measure with the exact note being shipped; a rewording could push it over.
|
|
490
475
|
const withPageOne = pageOne === undefined ? undefined : { ...withIndex, page_one: pageOne, note: budgetNote(meta, cleanup, true) };
|
|
491
476
|
if (withPageOne && fitsBudget(withPageOne))
|
|
492
477
|
return withPageOne;
|
|
@@ -499,8 +484,8 @@ function doneReceipt(artifact, input) {
|
|
|
499
484
|
return { ...base, note: `${meta.pages} 页超出预算,连页索引都放不下:用 pages="1-${OCR_MAX_PAGES_PER_READ}" 分段读,或用 query 定位` };
|
|
500
485
|
}
|
|
501
486
|
/**
|
|
502
|
-
*
|
|
503
|
-
*
|
|
487
|
+
* Budget note must be accurate: the budget covers text plus envelope and index,
|
|
488
|
+
* so say "text + receipt does not fit", not "text exceeds N".
|
|
504
489
|
*/
|
|
505
490
|
function budgetNote(meta, cleanup, withPageOne) {
|
|
506
491
|
const line = `整篇正文 ${meta.chars} 字符连同回执装不进 ${SOURCE_OUTPUT_BUDGET_CHARS} 字符预算:这里给${withPageOne ? '页索引与首页' : '页索引'},正文用 pages 或 query 取(不再出网、不再花钱)`;
|
|
@@ -519,7 +504,7 @@ function pendingReceipt(input) {
|
|
|
519
504
|
...echoFields(input.echo),
|
|
520
505
|
};
|
|
521
506
|
}
|
|
522
|
-
/** `pages`
|
|
507
|
+
/** `pages` form: whole pages; overflow drops whole pages from the tail. */
|
|
523
508
|
function pagesReceipt(input) {
|
|
524
509
|
const { meta, document } = input.artifact;
|
|
525
510
|
const envelope = (excerpts, omitted) => ({
|
|
@@ -538,7 +523,7 @@ function pagesReceipt(input) {
|
|
|
538
523
|
...(entry.heading === undefined ? {} : { heading: entry.heading }),
|
|
539
524
|
text: entry.text,
|
|
540
525
|
};
|
|
541
|
-
//
|
|
526
|
+
// First page is never skipped for budget: a single oversized page errors instead of half-shipping.
|
|
542
527
|
if (!fitsBudget(envelope([...excerpts, item], []))) {
|
|
543
528
|
omitted = input.requested.slice(excerpts.length);
|
|
544
529
|
break;
|
|
@@ -555,7 +540,7 @@ function pagesReceipt(input) {
|
|
|
555
540
|
...(notes.length > 0 ? { note: notes.join(';') } : {}),
|
|
556
541
|
};
|
|
557
542
|
}
|
|
558
|
-
/** `query`
|
|
543
|
+
/** `query` form: hit windows are trimmed from the tail, never a half list. */
|
|
559
544
|
function queryReceipt(input) {
|
|
560
545
|
const { meta, document } = input.artifact;
|
|
561
546
|
const found = locateQuery(document, input.query, input.limit, OCR_QUERY_WINDOW_CHARS);
|
|
@@ -583,7 +568,7 @@ function queryReceipt(input) {
|
|
|
583
568
|
note: notes.join(';'),
|
|
584
569
|
};
|
|
585
570
|
}
|
|
586
|
-
/**
|
|
571
|
+
/** Persist (content-addressed, no overwrite by default), then build the receipt. */
|
|
587
572
|
async function persistAndReceipt(deps, input) {
|
|
588
573
|
const meta = buildArtifactMeta({
|
|
589
574
|
doc_id: input.doc_id,
|
|
@@ -599,8 +584,8 @@ async function persistAndReceipt(deps, input) {
|
|
|
599
584
|
await writeOcrArtifact(deps.store, { session: input.session, meta, document: input.document, signal: input.signal, overwrite: input.refresh });
|
|
600
585
|
}
|
|
601
586
|
catch (error) {
|
|
602
|
-
//
|
|
603
|
-
//
|
|
587
|
+
// Concurrent writers race on createIfAbsent: read back the winner (same content
|
|
588
|
+
// address) instead of reporting a success as failure.
|
|
604
589
|
if (input.refresh)
|
|
605
590
|
throw error;
|
|
606
591
|
const existing = await readOcrArtifact(deps.store, { session: input.session, doc_id: input.doc_id, signal: input.signal });
|
|
@@ -611,16 +596,9 @@ async function persistAndReceipt(deps, input) {
|
|
|
611
596
|
return doneReceipt({ meta, document: input.document }, { cached: false, elapsed_ms: input.elapsed_ms, echo: input.echo });
|
|
612
597
|
}
|
|
613
598
|
/**
|
|
614
|
-
*
|
|
615
|
-
* `
|
|
616
|
-
*
|
|
617
|
-
* 形态归一化(`ocrRequest`)的失败**同样**要走这里:那一层的 `hint` 是"下一步该传什么",
|
|
618
|
-
* 抛裸 `Error` 就只剩一句中文,模型照旧会再猜一次。回声此时还没有(连请求都无法指认),
|
|
619
|
-
* 所以缺省不带,而不是硬编一个空标签冒充。
|
|
620
|
-
*
|
|
621
|
-
* `spent` 是**本次调用已经产生花费**的指认:`url`/`file` 形态的 `doc_id` 是算出来的、
|
|
622
|
-
* `job_id` 是提交回来的,都不在入参里。失败时把它们交回去,模型才能走"只传 job_id + doc_id
|
|
623
|
-
* 续查"那条不花钱的路;不给就等于把一次已经付费的作业丢在半路。
|
|
599
|
+
* Error receipt. Normalization failures also land here so their hint survives.
|
|
600
|
+
* `spent` returns the computed doc_id / submitted job_id for already-paid work,
|
|
601
|
+
* so the model can resume instead of paying again.
|
|
624
602
|
*/
|
|
625
603
|
function ocrErrorReceipt(error, input) {
|
|
626
604
|
const docId = input.requested?.doc_id ?? input.spent?.doc_id;
|
|
@@ -686,7 +664,7 @@ async function runOcr(deps, trace, args, exec) {
|
|
|
686
664
|
echo,
|
|
687
665
|
});
|
|
688
666
|
}
|
|
689
|
-
//
|
|
667
|
+
// Compute doc_id first; a cache hit makes zero upstream calls.
|
|
690
668
|
const target = requested.mode === 'url'
|
|
691
669
|
? await pdfUrlTarget(requested.url, signal, deps.resolveAddresses)
|
|
692
670
|
: await localFileTarget(deps, requested.file, session, signal);
|
|
@@ -718,15 +696,15 @@ async function runOcr(deps, trace, args, exec) {
|
|
|
718
696
|
});
|
|
719
697
|
}
|
|
720
698
|
catch (error) {
|
|
721
|
-
//
|
|
699
|
+
// Cancellation is the caller's intent: rethrow, do not wrap as upstream failure.
|
|
722
700
|
if (isAbortError(error))
|
|
723
701
|
throw error;
|
|
724
702
|
throwToolFailure(ocrErrorReceipt(error, { requested, echo, spent }));
|
|
725
703
|
}
|
|
726
704
|
}
|
|
727
705
|
/**
|
|
728
|
-
* `url`
|
|
729
|
-
*
|
|
706
|
+
* `url` form: reuse the local-fetch public-host gate, then require `.pdf`. We do
|
|
707
|
+
* not download the file (upstream does), so there is no size cap here, only SSRF.
|
|
730
708
|
*/
|
|
731
709
|
async function pdfUrlTarget(value, signal, resolveAddresses) {
|
|
732
710
|
const url = await assertPublicUrlTarget(value, signal, resolveAddresses);
|
|
@@ -736,31 +714,26 @@ async function pdfUrlTarget(value, signal, resolveAddresses) {
|
|
|
736
714
|
}
|
|
737
715
|
return { seed: `url:${url}`, source: `url:${url}`, sourceKind: 'url', submit: { fileUrl: url } };
|
|
738
716
|
}
|
|
739
|
-
/** `file`
|
|
717
|
+
/** `file` form: read bytes; provenance records the workspace-relative path only. */
|
|
740
718
|
async function localFileTarget(deps, value, session, signal) {
|
|
741
719
|
const path = normalizeLocalPath(value);
|
|
742
720
|
const { bytes, filename } = await readOcrSourceBytes(deps.store, { session, path, signal });
|
|
743
721
|
return { seed: bytes, source: `file:${path}`, sourceKind: 'file', submit: { file: { bytes, filename } } };
|
|
744
722
|
}
|
|
745
|
-
//
|
|
723
|
+
// Named-source query tools
|
|
746
724
|
//
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// 九个来源各自是一次具体实现,没有配方注册表 / 通用适配层——见 `sources.ts` 文件头。
|
|
725
|
+
// Query workface vs. search/fetch: source + params -> deterministic, ordered,
|
|
726
|
+
// pageable result set. Each source is concrete; see `sources.ts` header.
|
|
750
727
|
const SOURCE_RESULT_CAP = MAX_ITEMS;
|
|
751
728
|
const SOURCE_PAGE_SIZE_MAX = 50;
|
|
752
|
-
/**
|
|
729
|
+
/** Shared by six code-taking tools so description and validation cannot drift. */
|
|
753
730
|
const CODE_FORMS = '(也可写 SH600519 / 600519.SH,会归一化成 6 位)';
|
|
754
731
|
/**
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
* (由 `test/persona.test.mjs` 钉住与本常量同源)。超过阈值的工具结果在会话历史里会被
|
|
758
|
-
* `dsh-compaction-tool-result-pruner` 换成 head 4096 + `[... middle pruned ...]` + tail 1024,
|
|
759
|
-
* 也就是**中间条目被无声吃掉**,而留在头部的 `count` 还写着原来的条数。
|
|
760
|
-
* 留 25% 余量的做法与数据面能力目录的预算同一口径(`test/data-collector-hub.test.mjs`)。
|
|
732
|
+
* Source output budget = 75% of the host's 8192 tool-result pruning threshold.
|
|
733
|
+
* Over that, the middle is silently pruned while `count` still claims the full total.
|
|
761
734
|
*/
|
|
762
735
|
export const SOURCE_OUTPUT_BUDGET_CHARS = 6144;
|
|
763
|
-
/**
|
|
736
|
+
/** Headroom for the "N items omitted" note, which itself counts against the budget. */
|
|
764
737
|
const TRUNCATION_NOTE_RESERVE = 200;
|
|
765
738
|
function sourceOutput(outcome, recent, tally) {
|
|
766
739
|
const candidates = outcome.items.slice(0, SOURCE_RESULT_CAP);
|
|
@@ -781,7 +754,8 @@ function sourceOutput(outcome, recent, tally) {
|
|
|
781
754
|
provider_tally: tally,
|
|
782
755
|
};
|
|
783
756
|
};
|
|
784
|
-
//
|
|
757
|
+
// Trim from the tail in upstream order: skipping items breaks "same params, same
|
|
758
|
+
// result" and `count` would no longer be a prefix length.
|
|
785
759
|
let taken = 0;
|
|
786
760
|
let used = JSON.stringify(envelope([], 0)).length + TRUNCATION_NOTE_RESERVE;
|
|
787
761
|
for (const item of candidates) {
|
|
@@ -793,19 +767,15 @@ function sourceOutput(outcome, recent, tally) {
|
|
|
793
767
|
}
|
|
794
768
|
let items = candidates.slice(0, taken);
|
|
795
769
|
let payload = envelope(items, candidates.length - items.length);
|
|
796
|
-
//
|
|
797
|
-
//
|
|
770
|
+
// Safety net: if the reserved headroom was not enough (long note), drop one more
|
|
771
|
+
// item rather than let the pruner split the result.
|
|
798
772
|
while (items.length > 1 && JSON.stringify(payload).length > SOURCE_OUTPUT_BUDGET_CHARS) {
|
|
799
773
|
items = items.slice(0, -1);
|
|
800
774
|
payload = envelope(items, candidates.length - items.length);
|
|
801
775
|
}
|
|
802
776
|
return payload;
|
|
803
777
|
}
|
|
804
|
-
/**
|
|
805
|
-
* 失败也做成**工具错误**(`isError`),与 `web_retriever_fetch` 同一口径:
|
|
806
|
-
* 宿主用 `isError` 表达失败,正常返回无法标记失败(见 `throwToolFailure` 注释)。
|
|
807
|
-
* 抛出的消息带完整结构化回执,`recent_retrievals` / `provider_tally` 一个不丢。
|
|
808
|
-
*/
|
|
778
|
+
/** Same isError convention as `web_retriever_fetch`; receipt stays structured. */
|
|
809
779
|
function throwSourceFailure(source, operation, error, recent, tally) {
|
|
810
780
|
const code = error instanceof SourceError ? error.code : 'NETWORK';
|
|
811
781
|
throwToolFailure({
|
|
@@ -822,14 +792,9 @@ function sourceItemsText(items) {
|
|
|
822
792
|
return items.length === 0 ? '(0 条)' : `${items.length} 条`;
|
|
823
793
|
}
|
|
824
794
|
/**
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
* 不依赖 AnySearch / Wind;任何一路的配置缺失都不应连带影响另外几路的注册。
|
|
829
|
-
*
|
|
830
|
-
* `enabled`(设置卡片「允许启动本地提取网页内容」)**同时支配这九个工具的执行**:
|
|
831
|
-
* 关闭时工具仍在注册面(不静默消失),但每次调用响亮失败并说明开关位置——
|
|
832
|
-
* 卡片文案的字面语义对模型与用户都成立,用户同意不被旁路。
|
|
795
|
+
* Nine source tools depend only on HTTP egress, so they register independently of
|
|
796
|
+
* AnySearch/Wind config. `enabled=false` still registers but each call fails loudly
|
|
797
|
+
* naming the settings switch, so the switch semantics hold for user and model.
|
|
833
798
|
*/
|
|
834
799
|
export function registerSourceTools(ctx, localFetchOptions) {
|
|
835
800
|
const tools = ctx.get('tools');
|
|
@@ -837,11 +802,9 @@ export function registerSourceTools(ctx, localFetchOptions) {
|
|
|
837
802
|
return;
|
|
838
803
|
const echo = createRetrievalEcho();
|
|
839
804
|
const transport = createSourceTransport(localFetchOptions);
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
// 闸门实现(`createHttpRequester`):出口校验、逐跳重定向重校验、大小/超时上限与其余
|
|
844
|
-
// 六个来源同一份(此前走裸 fetch,是"声称一份、实际两份"的事故形态,AGENTS.md §9.7)。
|
|
805
|
+
// The three eastmoney tools hit one upstream from one egress IP, so they share one
|
|
806
|
+
// process-wide throttle and the same gated transport; a second bare fetch path is
|
|
807
|
+
// the "two implementations" regression (AGENTS.md §9.7).
|
|
845
808
|
const eastmoneyClient = createEastmoneyClient({
|
|
846
809
|
userAgent: localFetchOptions.userAgent,
|
|
847
810
|
throttle: sharedEastmoneyThrottle(),
|
|
@@ -877,9 +840,8 @@ export function registerSourceTools(ctx, localFetchOptions) {
|
|
|
877
840
|
outcome = await produce();
|
|
878
841
|
}
|
|
879
842
|
catch (error) {
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
// 传输层抛的是 `LocalFetchError('ABORTED')`,来源层抛的是 `SourceError('ABORTED')`。
|
|
843
|
+
// Caller cancel rethrows untouched (`LocalFetchError('ABORTED')` or
|
|
844
|
+
// `SourceError('ABORTED')`); only real failures become receipts.
|
|
883
845
|
const code = error instanceof SourceError ? error.code : error?.code;
|
|
884
846
|
if (code === 'ABORTED' || (error instanceof Error && error.name === 'AbortError'))
|
|
885
847
|
throw error;
|