@v587d/capital-generation 0.0.0-stage → 2.6.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 +794 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/assets/cg-icon.png +0 -0
- package/capital-config/client.js +28 -0
- package/capital-config/client.src.cjs +388 -0
- package/capital-config/index.js +113 -0
- package/capital-config/package.json +36 -0
- package/capital-watchlist/client.js +182 -0
- package/capital-watchlist/client.src.cjs +1229 -0
- package/capital-watchlist/index.js +772 -0
- package/capital-watchlist/package.json +37 -0
- package/chart-ui/README.md +112 -0
- package/chart-ui/client.js +16 -0
- package/chart-ui/client.src.cjs +126 -0
- package/chart-ui/index.js +205 -0
- package/chart-ui/package.json +30 -0
- package/cordis.patch.yml +72 -0
- package/lib/agents/bash-guard.d.ts +55 -0
- package/lib/agents/bash-guard.js +162 -0
- package/lib/agents/root-tool-policy.d.ts +164 -0
- package/lib/agents/root-tool-policy.js +309 -0
- package/lib/chart/artifact-ref.d.ts +65 -0
- package/lib/chart/artifact-ref.js +107 -0
- package/lib/chart/downsample.d.ts +8 -0
- package/lib/chart/downsample.js +53 -0
- package/lib/chart/errors.d.ts +16 -0
- package/lib/chart/errors.js +15 -0
- package/lib/chart/events.d.ts +120 -0
- package/lib/chart/events.js +471 -0
- package/lib/chart/html.d.ts +21 -0
- package/lib/chart/html.js +154 -0
- package/lib/chart/runtime.d.ts +20 -0
- package/lib/chart/runtime.js +270 -0
- package/lib/chart/series.d.ts +102 -0
- package/lib/chart/series.js +424 -0
- package/lib/chart/source-token.d.ts +64 -0
- package/lib/chart/source-token.js +217 -0
- package/lib/chart/spec.d.ts +58 -0
- package/lib/chart/spec.js +265 -0
- package/lib/chart/tool.d.ts +86 -0
- package/lib/chart/tool.js +288 -0
- package/lib/chart/vendor/LICENSE +201 -0
- package/lib/chart/vendor/VERSION.json +8 -0
- package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
- package/lib/data-collector/args.d.ts +38 -0
- package/lib/data-collector/args.js +114 -0
- package/lib/data-collector/dataset-tools.d.ts +3 -0
- package/lib/data-collector/dataset-tools.js +421 -0
- package/lib/data-collector/describe.d.ts +99 -0
- package/lib/data-collector/describe.js +256 -0
- package/lib/data-collector/hub.d.ts +187 -0
- package/lib/data-collector/hub.js +469 -0
- package/lib/data-collector/query-time.d.ts +58 -0
- package/lib/data-collector/query-time.js +101 -0
- package/lib/data-collector/query.d.ts +57 -0
- package/lib/data-collector/query.js +473 -0
- package/lib/data-collector/store.d.ts +499 -0
- package/lib/data-collector/store.js +1486 -0
- package/lib/data-collector/time-axis.d.ts +169 -0
- package/lib/data-collector/time-axis.js +419 -0
- package/lib/data-collector/tools.d.ts +32 -0
- package/lib/data-collector/tools.js +216 -0
- package/lib/index.d.ts +275 -0
- package/lib/index.js +458 -0
- package/lib/net/eastmoney-client.d.ts +104 -0
- package/lib/net/eastmoney-client.js +118 -0
- package/lib/net/throttle.d.ts +16 -0
- package/lib/net/throttle.js +34 -0
- package/lib/ocr/client.d.ts +82 -0
- package/lib/ocr/client.js +281 -0
- package/lib/ocr/markdown.d.ts +75 -0
- package/lib/ocr/markdown.js +209 -0
- package/lib/ocr/store.d.ts +119 -0
- package/lib/ocr/store.js +226 -0
- package/lib/sources/eastmoney-http.d.ts +2 -0
- package/lib/sources/eastmoney-http.js +1253 -0
- package/lib/sources/fuyao-core.d.ts +52 -0
- package/lib/sources/fuyao-core.js +96 -0
- package/lib/sources/fuyao-rest.d.ts +14 -0
- package/lib/sources/fuyao-rest.js +2057 -0
- package/lib/sources/security-code.d.ts +27 -0
- package/lib/sources/security-code.js +104 -0
- package/lib/sources/security-identity.d.ts +24 -0
- package/lib/sources/security-identity.js +89 -0
- package/lib/sources/tencent-http.d.ts +2 -0
- package/lib/sources/tencent-http.js +1053 -0
- package/lib/sources/wind-mcp.d.ts +8 -0
- package/lib/sources/wind-mcp.js +593 -0
- package/lib/time/tools.d.ts +74 -0
- package/lib/time/tools.js +464 -0
- package/lib/tool-exec.d.ts +29 -0
- package/lib/tool-exec.js +17 -0
- package/lib/web-retriever/engines.d.ts +61 -0
- package/lib/web-retriever/engines.js +202 -0
- package/lib/web-retriever/html-markdown.d.ts +50 -0
- package/lib/web-retriever/html-markdown.js +321 -0
- package/lib/web-retriever/local-fetch.d.ts +121 -0
- package/lib/web-retriever/local-fetch.js +562 -0
- package/lib/web-retriever/retriever.d.ts +58 -0
- package/lib/web-retriever/retriever.js +94 -0
- package/lib/web-retriever/sources.d.ts +235 -0
- package/lib/web-retriever/sources.js +961 -0
- package/lib/web-retriever/tools.d.ts +50 -0
- package/lib/web-retriever/tools.js +1029 -0
- package/lib/web-retriever/wind-client.d.ts +69 -0
- package/lib/web-retriever/wind-client.js +311 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +76 -4
- package/preset/capital-generation/README.md +161 -0
- package/preset/capital-generation/agent.patch.yml +509 -0
- package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
- package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wind 金融文档检索客户端(JSON-RPC over HTTP,零依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 线格式为 JSON-RPC 2.0(即 MCP 线协议):initialize → tools/call,无会话态、
|
|
5
|
+
* 无 SDK、无框架依赖——与 fuyao-rest / anysearch engines 同构的普通 HTTP 客户端。
|
|
6
|
+
* 协议蓝本(Wind 官方 CLI):github.com/Wind-Alice/AliceMarket 的
|
|
7
|
+
* skills/wind-mcp-skill/scripts/cli.mjs(本地参考副本在 docs/reference/,不入库)。
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_WIND_ENDPOINT = "https://mcp.wind.com.cn/vserver_financial_docs/mcp/";
|
|
10
|
+
export declare const WIND_REQUEST_TIMEOUT_MS = 60000;
|
|
11
|
+
export declare const WIND_MAX_CONTENT_CHARS = 20000;
|
|
12
|
+
/**
|
|
13
|
+
* 七台托管 MCP server 的端点(`server_type` → URL)。
|
|
14
|
+
*
|
|
15
|
+
* 这里是**事实**(端点常量),不是代码搬运:上游仓库未声明 LICENSE,蓝本
|
|
16
|
+
* (官方 CLI 的 `SERVERS` 表)按惯例只作本地参考副本、不入库,见
|
|
17
|
+
* `docs/design/wind-market-source.md` §1.1。
|
|
18
|
+
*
|
|
19
|
+
* `analytics_data` 一并列出是为了记下"它存在但不可用":2026-10-05 实测连 `initialize`
|
|
20
|
+
* 都挂满 60 秒(同文档 §6.1),所以数据源侧不接它。
|
|
21
|
+
*/
|
|
22
|
+
export declare const WIND_SERVER_ENDPOINTS: {
|
|
23
|
+
readonly stock_data: "https://mcp.wind.com.cn/vserver_stock_data/mcp/";
|
|
24
|
+
readonly fund_data: "https://mcp.wind.com.cn/vserver_fund_data/mcp/";
|
|
25
|
+
readonly index_data: "https://mcp.wind.com.cn/vserver_index_data/mcp/";
|
|
26
|
+
readonly bond_data: "https://mcp.wind.com.cn/vserver_bond_data/mcp/";
|
|
27
|
+
readonly financial_docs: "https://mcp.wind.com.cn/vserver_financial_docs/mcp/";
|
|
28
|
+
readonly economic_data: "https://mcp.wind.com.cn/vserver_economic_data/mcp/";
|
|
29
|
+
readonly analytics_data: "https://mcp.wind.com.cn/vserver_analytics_data/mcp/";
|
|
30
|
+
};
|
|
31
|
+
export type WindServerType = keyof typeof WIND_SERVER_ENDPOINTS;
|
|
32
|
+
/**
|
|
33
|
+
* 错误分类(persona/主 Agent 依据它决定 fallback):
|
|
34
|
+
* AUTH=Key 缺失或无效;RATE_LIMIT=429;NETWORK=网络错误/5xx/超时;
|
|
35
|
+
* BACKEND=接口层与业务错误;INVALID=响应不可解析。
|
|
36
|
+
*/
|
|
37
|
+
export type WindErrorCode = 'AUTH' | 'RATE_LIMIT' | 'NETWORK' | 'BACKEND' | 'INVALID';
|
|
38
|
+
export interface WindCallResult {
|
|
39
|
+
ok: boolean;
|
|
40
|
+
/** 成功且正文可解析为 JSON 时:Wind 返回结果(字符串字段裁剪到 WIND_MAX_CONTENT_CHARS)。 */
|
|
41
|
+
data?: unknown;
|
|
42
|
+
/** 成功但正文非 JSON 时:原文透传(≤WIND_MAX_CONTENT_CHARS)。 */
|
|
43
|
+
content?: string;
|
|
44
|
+
content_chars?: number;
|
|
45
|
+
error?: string;
|
|
46
|
+
code?: WindErrorCode;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 报给 Wind 的客户端版本(`initialize.clientInfo.version` 与 `tools/call._meta.clientVersion`)。
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ 必须与 `package.json` 的 `version` 一致:嵌套包 `chart-ui/package.json` 的版本由
|
|
52
|
+
* `test/chart-composition.test.mjs` 守着(会因漂移而失败),这里原本是写死的第二处,
|
|
53
|
+
* 一次发版就得记得改两遍。所以本常量由测试一并核对(见 `test/wind-client.test.mjs`)。
|
|
54
|
+
*/
|
|
55
|
+
export declare const WIND_CLIENT_VERSION = "2.6.0";
|
|
56
|
+
export interface WindClient {
|
|
57
|
+
callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
|
|
58
|
+
}
|
|
59
|
+
export interface WindClientOptions {
|
|
60
|
+
endpoint?: string;
|
|
61
|
+
/** 每次调用重新解析 Key,不缓存(与 anysearch engines 同策略)。 */
|
|
62
|
+
resolveApiKey: () => Promise<string | undefined>;
|
|
63
|
+
/** 单次请求超时毫秒(initialize 与 tools/call 各自适用);默认 60000。 */
|
|
64
|
+
timeoutMs?: number;
|
|
65
|
+
/** 网络错误重试间隔毫秒;默认 [300, 1000],仅供测试注入缩短。 */
|
|
66
|
+
retryDelaysMs?: number[];
|
|
67
|
+
}
|
|
68
|
+
/** 创建 Wind 文档检索客户端;端点在构造时校验(配置错误立即暴露,与 anysearch 同策略)。 */
|
|
69
|
+
export declare function createWindClient(options: WindClientOptions): WindClient;
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wind 金融文档检索客户端(JSON-RPC over HTTP,零依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 线格式为 JSON-RPC 2.0(即 MCP 线协议):initialize → tools/call,无会话态、
|
|
5
|
+
* 无 SDK、无框架依赖——与 fuyao-rest / anysearch engines 同构的普通 HTTP 客户端。
|
|
6
|
+
* 协议蓝本(Wind 官方 CLI):github.com/Wind-Alice/AliceMarket 的
|
|
7
|
+
* skills/wind-mcp-skill/scripts/cli.mjs(本地参考副本在 docs/reference/,不入库)。
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_WIND_ENDPOINT = 'https://mcp.wind.com.cn/vserver_financial_docs/mcp/';
|
|
10
|
+
export const WIND_REQUEST_TIMEOUT_MS = 60_000;
|
|
11
|
+
export const WIND_MAX_CONTENT_CHARS = 20_000;
|
|
12
|
+
/**
|
|
13
|
+
* 七台托管 MCP server 的端点(`server_type` → URL)。
|
|
14
|
+
*
|
|
15
|
+
* 这里是**事实**(端点常量),不是代码搬运:上游仓库未声明 LICENSE,蓝本
|
|
16
|
+
* (官方 CLI 的 `SERVERS` 表)按惯例只作本地参考副本、不入库,见
|
|
17
|
+
* `docs/design/wind-market-source.md` §1.1。
|
|
18
|
+
*
|
|
19
|
+
* `analytics_data` 一并列出是为了记下"它存在但不可用":2026-10-05 实测连 `initialize`
|
|
20
|
+
* 都挂满 60 秒(同文档 §6.1),所以数据源侧不接它。
|
|
21
|
+
*/
|
|
22
|
+
export const WIND_SERVER_ENDPOINTS = {
|
|
23
|
+
stock_data: 'https://mcp.wind.com.cn/vserver_stock_data/mcp/',
|
|
24
|
+
fund_data: 'https://mcp.wind.com.cn/vserver_fund_data/mcp/',
|
|
25
|
+
index_data: 'https://mcp.wind.com.cn/vserver_index_data/mcp/',
|
|
26
|
+
bond_data: 'https://mcp.wind.com.cn/vserver_bond_data/mcp/',
|
|
27
|
+
financial_docs: DEFAULT_WIND_ENDPOINT,
|
|
28
|
+
economic_data: 'https://mcp.wind.com.cn/vserver_economic_data/mcp/',
|
|
29
|
+
analytics_data: 'https://mcp.wind.com.cn/vserver_analytics_data/mcp/',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 报给 Wind 的客户端版本(`initialize.clientInfo.version` 与 `tools/call._meta.clientVersion`)。
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ 必须与 `package.json` 的 `version` 一致:嵌套包 `chart-ui/package.json` 的版本由
|
|
35
|
+
* `test/chart-composition.test.mjs` 守着(会因漂移而失败),这里原本是写死的第二处,
|
|
36
|
+
* 一次发版就得记得改两遍。所以本常量由测试一并核对(见 `test/wind-client.test.mjs`)。
|
|
37
|
+
*/
|
|
38
|
+
export const WIND_CLIENT_VERSION = '2.6.0';
|
|
39
|
+
function errorText(error) {
|
|
40
|
+
return error instanceof Error ? error.message : String(error);
|
|
41
|
+
}
|
|
42
|
+
class WindTransportError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.code = code;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** HTTP 状态 → 错误分类(蓝本 HTTP_ERROR_MAP;其余状态一律 NETWORK)。 */
|
|
50
|
+
const HTTP_ERROR_CODES = {
|
|
51
|
+
401: 'AUTH',
|
|
52
|
+
429: 'RATE_LIMIT',
|
|
53
|
+
500: 'NETWORK',
|
|
54
|
+
502: 'NETWORK',
|
|
55
|
+
503: 'NETWORK',
|
|
56
|
+
504: 'NETWORK',
|
|
57
|
+
};
|
|
58
|
+
const RETRY_ATTEMPTS = 3;
|
|
59
|
+
const DEFAULT_RETRY_DELAYS_MS = [300, 1_000];
|
|
60
|
+
function normalizeEndpoint(endpoint) {
|
|
61
|
+
const raw = (endpoint ?? DEFAULT_WIND_ENDPOINT).trim();
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(raw);
|
|
64
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
65
|
+
throw new Error('endpoint must use http or https');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new Error(`invalid Wind endpoint: ${errorText(error)}`);
|
|
70
|
+
}
|
|
71
|
+
return raw;
|
|
72
|
+
}
|
|
73
|
+
/** 响应可能是纯 JSON 或 SSE(取最后一个 data: 行);蓝本 parseSSE 的等价实现。 */
|
|
74
|
+
function parseResponsePayload(text) {
|
|
75
|
+
const trimmed = text.trim();
|
|
76
|
+
if (trimmed.startsWith('{')) {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(trimmed);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// 非 JSON 对象落到下面的 SSE 解析。
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let last;
|
|
85
|
+
for (const line of text.split(/\r?\n/)) {
|
|
86
|
+
if (line.startsWith('data: '))
|
|
87
|
+
last = line.slice('data: '.length);
|
|
88
|
+
}
|
|
89
|
+
if (last !== undefined) {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(last);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new Error(`SSE data 行 JSON 解析失败:${errorText(error)};原文前 200 字符:${text.slice(0, 200)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`响应格式无法识别(既非 JSON 也非 SSE);原文前 200 字符:${text.slice(0, 200)}`);
|
|
98
|
+
}
|
|
99
|
+
/** 深度裁剪:对象/数组内的字符串字段超过上限时截断(保持结构;超长响应另有 harness 剪枝兜底)。 */
|
|
100
|
+
function trimDeep(value, maxChars) {
|
|
101
|
+
if (typeof value === 'string')
|
|
102
|
+
return value.length > maxChars ? value.slice(0, maxChars) : value;
|
|
103
|
+
if (Array.isArray(value))
|
|
104
|
+
return value.map((item) => trimDeep(item, maxChars));
|
|
105
|
+
if (value && typeof value === 'object') {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [key, item] of Object.entries(value)) {
|
|
108
|
+
out[key] = trimDeep(item, maxChars);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function firstContentText(result) {
|
|
115
|
+
const content = result.content;
|
|
116
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
const first = content[0];
|
|
119
|
+
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
|
120
|
+
const record = first;
|
|
121
|
+
if (record.type === 'text' && typeof record.text === 'string')
|
|
122
|
+
return record.text;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
/** JSON-RPC error 字段 → 可读文本。 */
|
|
127
|
+
function rpcErrorText(payload) {
|
|
128
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
129
|
+
return undefined;
|
|
130
|
+
const record = payload;
|
|
131
|
+
if (record.error === undefined || record.error === null)
|
|
132
|
+
return undefined;
|
|
133
|
+
const error = record.error;
|
|
134
|
+
if (typeof error === 'string' && error)
|
|
135
|
+
return error;
|
|
136
|
+
if (error && typeof error === 'object' && !Array.isArray(error)) {
|
|
137
|
+
const message = error.message;
|
|
138
|
+
if (typeof message === 'string' && message)
|
|
139
|
+
return message;
|
|
140
|
+
}
|
|
141
|
+
return JSON.stringify(error).slice(0, 2_000);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 三层业务错误解包(蓝本 mcpRequest 后半段):Wind 把业务错误包在 content[0].text
|
|
145
|
+
* 的 JSON 字符串里,必须二次解析——mcp_tool_error_code / error{} / data.code 三种形态。
|
|
146
|
+
*/
|
|
147
|
+
function innerBusinessError(data) {
|
|
148
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
149
|
+
return undefined;
|
|
150
|
+
const record = data;
|
|
151
|
+
const toolErrorCode = record.mcp_tool_error_code;
|
|
152
|
+
if (typeof toolErrorCode === 'number' && toolErrorCode !== 0) {
|
|
153
|
+
return typeof record.mcp_tool_error_msg === 'string' && record.mcp_tool_error_msg
|
|
154
|
+
? record.mcp_tool_error_msg
|
|
155
|
+
: JSON.stringify(record).slice(0, 2_000);
|
|
156
|
+
}
|
|
157
|
+
if (record.error && typeof record.error === 'object' && !Array.isArray(record.error)) {
|
|
158
|
+
const nested = record.error;
|
|
159
|
+
if (nested.code !== undefined || nested.message !== undefined) {
|
|
160
|
+
return (typeof nested.message === 'string' && nested.message) || JSON.stringify(nested).slice(0, 2_000);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (record.data && typeof record.data === 'object' && !Array.isArray(record.data)) {
|
|
164
|
+
const nested = record.data;
|
|
165
|
+
const numeric = typeof nested.code === 'number'
|
|
166
|
+
? nested.code
|
|
167
|
+
: typeof nested.code === 'string' && /^\d+$/.test(nested.code.trim())
|
|
168
|
+
? Number(nested.code.trim())
|
|
169
|
+
: null;
|
|
170
|
+
const success = numeric === null || numeric === 0 || (numeric >= 200 && numeric < 300);
|
|
171
|
+
if (!success) {
|
|
172
|
+
return (typeof nested.message === 'string' && nested.message) || JSON.stringify(nested).slice(0, 2_000);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
/** JSON-RPC result → WindCallResult:含 isError 与内层业务错误解包。 */
|
|
178
|
+
function interpretResult(payload) {
|
|
179
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
180
|
+
return { ok: false, code: 'INVALID', error: 'Wind 返回不可解析(非 JSON-RPC 对象)' };
|
|
181
|
+
}
|
|
182
|
+
const rpcError = rpcErrorText(payload);
|
|
183
|
+
if (rpcError)
|
|
184
|
+
return { ok: false, code: 'BACKEND', error: rpcError };
|
|
185
|
+
const result = payload.result;
|
|
186
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
187
|
+
return { ok: false, code: 'INVALID', error: 'Wind 返回缺少 result' };
|
|
188
|
+
}
|
|
189
|
+
const resultRecord = result;
|
|
190
|
+
if (resultRecord.isError === true) {
|
|
191
|
+
const text = firstContentText(resultRecord);
|
|
192
|
+
return { ok: false, code: 'BACKEND', error: text ?? JSON.stringify(resultRecord).slice(0, 2_000) };
|
|
193
|
+
}
|
|
194
|
+
const innerText = firstContentText(resultRecord);
|
|
195
|
+
if (innerText === undefined) {
|
|
196
|
+
return { ok: false, code: 'INVALID', error: 'Wind 返回缺少 content[0].text 文本' };
|
|
197
|
+
}
|
|
198
|
+
let parsed;
|
|
199
|
+
try {
|
|
200
|
+
parsed = JSON.parse(innerText);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
parsed = undefined;
|
|
204
|
+
}
|
|
205
|
+
if (parsed === undefined) {
|
|
206
|
+
const content = innerText.slice(0, WIND_MAX_CONTENT_CHARS);
|
|
207
|
+
return { ok: true, content, content_chars: content.length };
|
|
208
|
+
}
|
|
209
|
+
const businessError = innerBusinessError(parsed);
|
|
210
|
+
if (businessError)
|
|
211
|
+
return { ok: false, code: 'BACKEND', error: businessError };
|
|
212
|
+
return { ok: true, data: trimDeep(parsed, WIND_MAX_CONTENT_CHARS) };
|
|
213
|
+
}
|
|
214
|
+
/** 创建 Wind 文档检索客户端;端点在构造时校验(配置错误立即暴露,与 anysearch 同策略)。 */
|
|
215
|
+
export function createWindClient(options) {
|
|
216
|
+
const endpoint = normalizeEndpoint(options.endpoint);
|
|
217
|
+
const timeoutMs = options.timeoutMs && options.timeoutMs > 0 ? options.timeoutMs : WIND_REQUEST_TIMEOUT_MS;
|
|
218
|
+
const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
219
|
+
let nextId = 1;
|
|
220
|
+
async function postOnce(method, params, apiKey, signal) {
|
|
221
|
+
if (signal?.aborted)
|
|
222
|
+
throw new WindTransportError('NETWORK', 'Wind 请求已取消');
|
|
223
|
+
const controller = new AbortController();
|
|
224
|
+
const onExternalAbort = () => controller.abort(signal?.reason);
|
|
225
|
+
signal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
226
|
+
let timedOut = false;
|
|
227
|
+
const timer = setTimeout(() => {
|
|
228
|
+
timedOut = true;
|
|
229
|
+
controller.abort(new Error('Wind request timed out'));
|
|
230
|
+
}, timeoutMs);
|
|
231
|
+
try {
|
|
232
|
+
const response = await fetch(endpoint, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
redirect: 'error',
|
|
235
|
+
headers: {
|
|
236
|
+
accept: 'application/json, text/event-stream',
|
|
237
|
+
'content-type': 'application/json',
|
|
238
|
+
authorization: `Bearer ${apiKey}`,
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: nextId++, method, params }),
|
|
241
|
+
signal: controller.signal,
|
|
242
|
+
});
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
await response.text().catch(() => '');
|
|
245
|
+
throw new WindTransportError(HTTP_ERROR_CODES[response.status] ?? 'NETWORK', `Wind HTTP ${response.status}`);
|
|
246
|
+
}
|
|
247
|
+
return parseResponsePayload(await response.text());
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (timedOut)
|
|
251
|
+
throw new WindTransportError('NETWORK', `Wind 请求超时(${timeoutMs}ms)`);
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
signal?.removeEventListener('abort', onExternalAbort);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** 网络级错误按退避重试;HTTP 状态错误与调用方取消不重试(蓝本同款行为)。 */
|
|
260
|
+
async function postRpc(method, params, apiKey, signal) {
|
|
261
|
+
let lastError;
|
|
262
|
+
for (let attempt = 1; attempt <= RETRY_ATTEMPTS; attempt += 1) {
|
|
263
|
+
try {
|
|
264
|
+
return await postOnce(method, params, apiKey, signal);
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
if (error instanceof WindTransportError)
|
|
268
|
+
throw error;
|
|
269
|
+
if (signal?.aborted)
|
|
270
|
+
throw error;
|
|
271
|
+
lastError = error;
|
|
272
|
+
if (attempt < RETRY_ATTEMPTS) {
|
|
273
|
+
const delayMs = retryDelaysMs[Math.min(attempt - 1, retryDelaysMs.length - 1)];
|
|
274
|
+
if (delayMs > 0)
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
throw new WindTransportError('NETWORK', `Wind 请求失败(已重试 ${RETRY_ATTEMPTS - 1} 次):${errorText(lastError)}`);
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
async callTool(toolName, args, signal) {
|
|
283
|
+
try {
|
|
284
|
+
const apiKey = await options.resolveApiKey();
|
|
285
|
+
if (!apiKey) {
|
|
286
|
+
return { ok: false, code: 'AUTH', error: 'Wind API Key 未配置(credentials 与环境变量均未提供)' };
|
|
287
|
+
}
|
|
288
|
+
const initialized = await postRpc('initialize', {
|
|
289
|
+
protocolVersion: '2025-03-26',
|
|
290
|
+
capabilities: {},
|
|
291
|
+
clientInfo: { name: 'capital-generation', version: WIND_CLIENT_VERSION },
|
|
292
|
+
}, apiKey, signal);
|
|
293
|
+
const initError = rpcErrorText(initialized);
|
|
294
|
+
if (initError)
|
|
295
|
+
return { ok: false, code: 'BACKEND', error: initError };
|
|
296
|
+
const payload = await postRpc('tools/call', {
|
|
297
|
+
name: toolName,
|
|
298
|
+
arguments: args,
|
|
299
|
+
_meta: { clientVersion: WIND_CLIENT_VERSION },
|
|
300
|
+
}, apiKey, signal);
|
|
301
|
+
return interpretResult(payload);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (error instanceof WindTransportError) {
|
|
305
|
+
return { ok: false, code: error.code, error: error.message };
|
|
306
|
+
}
|
|
307
|
+
return { ok: false, code: 'INVALID', error: `Wind 调用失败:${errorText(error)}` };
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@v587d/capital-generation",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "Capital Generation:面向中国散户的 AI 投资助手插件(Capital mode Agent Preset)",
|
|
5
|
+
"icon": "assets/cg-icon.png",
|
|
6
|
+
"author": "v587d",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./lib/index.js",
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/index.d.ts",
|
|
14
|
+
"import": "./lib/index.js",
|
|
15
|
+
"default": "./lib/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./locale/*.json": "./locale/*.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lib",
|
|
22
|
+
"locale",
|
|
23
|
+
"chart-ui",
|
|
24
|
+
"capital-config",
|
|
25
|
+
"capital-watchlist",
|
|
26
|
+
"preset",
|
|
27
|
+
"cordis.patch.yml",
|
|
28
|
+
"assets/cg-icon.png",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": [
|
|
35
|
+
"./cordis.patch.yml",
|
|
36
|
+
"./preset/capital-generation/agent.patch.yml"
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"clean": "node -e \"import('node:fs').then(({ rmSync }) => rmSync('lib', { recursive: true, force: true }))\"",
|
|
42
|
+
"build": "npm run clean && tsc && node scripts/copy-assets.mjs && npm run build:client && npm run build:config-client && npm run build:watchlist-client",
|
|
43
|
+
"build:client": "node scripts/build-client.mjs",
|
|
44
|
+
"build:config-client": "node scripts/build-capital-config-client.mjs",
|
|
45
|
+
"build:watchlist-client": "node scripts/build-capital-watchlist-client.mjs",
|
|
46
|
+
"dev": "tsc -w",
|
|
47
|
+
"vendor:charts": "node scripts/vendor-lightweight-charts.mjs",
|
|
48
|
+
"check:dsh": "node scripts/check-dsh-compat.mjs",
|
|
49
|
+
"demo:chart": "npm run build && node scripts/chart-demo.mjs",
|
|
50
|
+
"smoke:boot": "node scripts/smoke-boot.mjs",
|
|
51
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
52
|
+
"prepublishOnly": "npm run build",
|
|
53
|
+
"smoke:fuyao": "node scripts/smoke-fuyao.mjs",
|
|
54
|
+
"smoke:wind": "node scripts/smoke-wind.mjs",
|
|
55
|
+
"smoke:local-fetch": "node scripts/smoke-local-fetch.mjs",
|
|
56
|
+
"docs:capabilities": "npm run build && node scripts/gen-capability-table.mjs",
|
|
57
|
+
"verify:sessions": "node scripts/verify-sessions.mjs",
|
|
58
|
+
"audit:tool-errors": "node scripts/audit-tool-errors.mjs"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
65
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.2",
|
|
66
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
67
|
+
"esbuild": "^0.28.2",
|
|
68
|
+
"js-yaml": "^5.4.1",
|
|
69
|
+
"lightweight-charts": "5.2.1",
|
|
70
|
+
"typescript": "^5.0.0"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
74
|
+
"@joplin/turndown-plugin-gfm": "^1.0.68",
|
|
75
|
+
"turndown": "^7.2.4",
|
|
76
|
+
"zod": "^4.5.4"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Capital 模式 preset:组合设计说明
|
|
2
|
+
|
|
3
|
+
本文件是 `agent.patch.yml`(那颗 `@deepseek-ai/dsh-agent-preset` 声明行)的**维护者文档**(人类可读),
|
|
4
|
+
不是模型可见内容:`skill-filesystem` 只扫 `skills/`,本文件既不会进 skill 目录,也不会被注入上下文。
|
|
5
|
+
组合文件里只留"就地必需"的一行注(realm、`!!js`、disabled 行),其余设计理由都在这里。
|
|
6
|
+
|
|
7
|
+
- 本目录由 `@v587d/capital-generation` 随包分发,`agent.patch.yml` 走 `package.json` 的
|
|
8
|
+
`dsh.bundle.patch`(**数组**:主 patch + 这颗)以 `trust=system` 挂载;**不要**手抄进
|
|
9
|
+
`~/.dsh/.agent-presets`。
|
|
10
|
+
- 长协议在 `skills/`(同目录),由本 preset 自己的 `skill-filesystem` 行发现;`capital-visualization-protocol` 同时供 data_junior 的 gate 和 visualization_specialist 按需加载。
|
|
11
|
+
- visualization_specialist 是 data_junior 创建的 one-shot 前台子 Agent,不进入主 Agent 的 direct-child 复用清单;它只消费 chart_source_ref 并使用受控 render_chart。
|
|
12
|
+
- 验证方式与"改 preset 前后的必做检查"见仓库根 `AGENTS.md` §8.3;设计理由与「不许改」全表见
|
|
13
|
+
`docs/dev/preset-persona.md`。
|
|
14
|
+
|
|
15
|
+
## 1. 平面纪律(plane discipline)
|
|
16
|
+
|
|
17
|
+
- `subagents` 注册表与它的 spawn/fork 后端住在 **host 组合**;本 preset 只贡献模型侧的
|
|
18
|
+
委派工具行,它们解析 host 那一份注册表。本文件**没有**挂 `subagent_fork` 行,所以委派
|
|
19
|
+
永远是 continuable 的 `spawn` provider。
|
|
20
|
+
- 本 preset 拥有的服务必须落在组行的 `isolate` realm 内(见第 4 节);只消费 host 能力的
|
|
21
|
+
行必须留在 realm 外,否则解析不到服务。
|
|
22
|
+
|
|
23
|
+
## 2. 人设(persona)承载与官方语义
|
|
24
|
+
|
|
25
|
+
实测 `dsh 0.1.5-rc.1`,`0.1.7-rc.2` 复核字段未变(`test/persona.test.mjs` 对着本机 dsh 核对),
|
|
26
|
+
三层载体注册的是**同一个 system-prompt 节名**:
|
|
27
|
+
|
|
28
|
+
| 载体 | 位置 | 字段 | 生效范围 |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| `dsh-system-prompt` 行 | 部署(web profile) | `personaPrefix` / `personaSuffix` | 全局 |
|
|
31
|
+
| `dsh-persona` 行 | 本 preset | `prefix`(必填)/ `suffix` / `complete` / `includeRuntimeContext` | preset 内全部 agent(主 + 子) |
|
|
32
|
+
| `dsh-tool-subagent` 行 | 本 preset 委派行 | `config.persona` | 该子 Agent 自己 |
|
|
33
|
+
|
|
34
|
+
- 节名是 `deployment:persona-prefix`(order 0)与 `deployment:persona-suffix`(order
|
|
35
|
+
10200)。**最近 scope 胜出 ⇒ 是覆盖不是追加**;`dsh-subagent` 把 `config.persona` 注册进
|
|
36
|
+
子 Agent 自己的 scope,所以子人设**替换**(不是扩展)主 persona。
|
|
37
|
+
- `prefix`/`suffix` 是模板,渲染时对 `{{…}}` **严格插值**:未注册或 undefined 的变量会抛错。
|
|
38
|
+
`provider` / `model` / `cwd` 由 `dsh-agent-loop` 全局注册,可以直接用。
|
|
39
|
+
- 本 preset **不设 `suffix`**:按官方语义,空模板**仍然遮蔽**部署级后缀,所以部署那句
|
|
40
|
+
`Your working directory is {{cwd}}.` 在 Capital 会话里不出现(主 persona 自己写了
|
|
41
|
+
`{{cwd}}`)。这是有意为之,不是漏配。
|
|
42
|
+
- `complete: true` 会把其余所有段落(身份、工具引导、后缀)一并抑制;`includeRuntimeContext:
|
|
43
|
+
false` 全有全无地关掉沙箱/审批/委派等 runtime context。本 preset 都不用。
|
|
44
|
+
- 成本:persona 正文每个请求都重复;`prefix` 变化会让 KV cache 从第一个变化 token 起失效。
|
|
45
|
+
所以正文按"每轮都要生效"的门槛收,参考材料一律外置。
|
|
46
|
+
|
|
47
|
+
## 3. 按需协议:什么进 persona、什么进 skills/
|
|
48
|
+
|
|
49
|
+
- **必须常驻 persona**:每轮预检、复用/创建判定、路由、权限边界、叶子边界、安全底线、
|
|
50
|
+
第一条 duty(先加载协议)、最小载荷骨架。这些要在"模型还不知道本轮要做什么"之前生效,
|
|
51
|
+
而 skill 只在模型决定加载时才生效。
|
|
52
|
+
- **按需进 `skills/`**:载荷 JSON 样例、字段级规则、`error/code → 处置`表、四态事实怎么读、
|
|
53
|
+
QuerySpec 细则、检索收敛法则、官方域名核验清单、回传格式范例。
|
|
54
|
+
- **子 Agent 也能按需加载**:子行的 `toolFilter.allow` 里写了 `skill`,子 Agent 就同时拿到
|
|
55
|
+
目录与加载器。判据在 `dsh-tools` 的 `view()`:restriction 过滤**继承层(全局 + 全部祖先
|
|
56
|
+
scope,含 preset 层)**,只豁免"本 scope 自己注册的"工具;而 `dsh-tool-skill` 发布目录的
|
|
57
|
+
条件正是 `ctx.tools.get('skill', agent) === 该注册对象`。源码注释里记着这段历史坑
|
|
58
|
+
("Once presets moved them onto the agent plane they became an ANCESTOR contribution, so a
|
|
59
|
+
child's filter silently stopped constraining anything it was given")——它既是"子 Agent
|
|
60
|
+
默认看不到 skill"的原因,也是放开它的唯一开关。
|
|
61
|
+
- **维护者理由不进 `skills/`**:任何放在 `skills/` 的东西都会拿到一条常驻目录项
|
|
62
|
+
(name + ≤500 字 description)并可被模型加载。设计理由写在本文件。
|
|
63
|
+
|
|
64
|
+
## 4. realm 与服务归属
|
|
65
|
+
|
|
66
|
+
- `capital-generation-scope`(`cordis:group` + `isolate: { dataCollectorHub: true,
|
|
67
|
+
datasetStore: true }`):插件 `apply` 里 `ctx.provide` 的两个服务是进程级名字,
|
|
68
|
+
必须按 session 隔离,否则第二个 Capital 会话注册时冲突。
|
|
69
|
+
- `compaction` 组(`isolate: { compaction: true, toolResultPruner: true }`):
|
|
70
|
+
`compaction-basic` 通过 `ctx.get` 读 `toolResultPruner`,两者必须同处一个 realm;
|
|
71
|
+
`tokenMeter` 留在宿主平面。
|
|
72
|
+
- skills 注册表在 **host 平面并按 scope 分层**:`skill-filesystem` / `tool-skill` 只注册进
|
|
73
|
+
本 preset 那一层,**不需要** realm。
|
|
74
|
+
- **图表前端组件(`capital-charts` 行)刻意在 host 平面、且不在任何 realm 里**:
|
|
75
|
+
客户端 bundle 必须在页面启动时就存在(boot graph 在 index.html 渲染时确定),
|
|
76
|
+
所以它不能是本 preset 的行。反过来,本插件的 `apply` 通过 `ctx.get('capitalCharts')`
|
|
77
|
+
消费它——`isolate` 只重映射**列出**的服务名(`cordis-plugin-loader/src/config/isolate.ts`:
|
|
78
|
+
`if (!label) return`),其余名字沿原型链向外解析,因此跨平面消费成立。
|
|
79
|
+
这一条与"realm 里的行不能消费外面的服务"并不矛盾:那条规则说的是**提供方**在 realm 内、
|
|
80
|
+
消费方在 realm 外的情况。
|
|
81
|
+
- **时间轴换算不在 preset 里,在宿主插件里**(`src/data-collector/time-axis.ts`)。理由与
|
|
82
|
+
"为什么这几个服务归宿主"同源:它是**确定性计算**,且**有三个入口**必须共用同一份答案——
|
|
83
|
+
profile 写出的 `time_facts`(`store.ts`)、工具边界收到的日期筛选值(`dataset-tools.ts`)、
|
|
84
|
+
`resolve_data_time_range` 的 Dataset 形态(`time/tools.ts`)。写进 persona 或 skill 只会变成
|
|
85
|
+
"请模型自己算"的口头约定(实测单步思考 41,961 字符全是时间戳算术);在 preset 侧再定义一个
|
|
86
|
+
模型工具则会把同一套偏移推断实现第二遍。放进宿主后,模型只需要写 `2026-08-23`。
|
|
87
|
+
|
|
88
|
+
## 5. 各行的作用与坑
|
|
89
|
+
|
|
90
|
+
- `agent-instructions`:注入 `AGENTS.md` / `CLAUDE.md`(宿主行在本 profile 被关,由 preset
|
|
91
|
+
自持,`maxBytes: 16384`)。没有它,工作区约定对 Capital 会话不可见。
|
|
92
|
+
- `tool-result-pruner`:`thresholdChars: 8192`(head 4096 + tail 1024)。真实后果是
|
|
93
|
+
**能力目录与工具结果超过阈值时中间段被剪掉**;`test/data-collector-hub.test.mjs` 因此用
|
|
94
|
+
"目录 6144 + 单能力详情 4096"两道测试预算,让测试先失败而不是运行时静默截断。该阈值
|
|
95
|
+
**不能按 Agent 区分**(全仓只有这一个 pruner 实例),主/子 Agent 共用。
|
|
96
|
+
- `toolFilter.allow` 只能写**真实注册**的全局工具名:`tools.restrict()` 对未知名字直接报错,
|
|
97
|
+
且它在**子 Agent 创建窗口**执行——写错的表现是"创建子 Agent 失败",不是挂载失败。
|
|
98
|
+
- **通用 `subagent` 行必须写 `toolFilter.deny`(2026-09 review)**:`dsh-subagent` 只在
|
|
99
|
+
`composition.toolFilter !== void 0` 时才 `restrict`,而 child 用 `composeFrom(childCtx,
|
|
100
|
+
parent.ctx)` 加入父 Agent **同一份 standing composition**,因此没有 filter 的通用 child
|
|
101
|
+
会继承 standing 的全部工具——只带 `parentSession` 的 child 就能自己 `prepare_chart_source`
|
|
102
|
+
再 `render_chart`,把"出图只有一个入口"降级成 persona 建议。这里的 `deny` 与子角色的
|
|
103
|
+
`allow` 不冲突:`allow`/`deny` 都只是继承层交集过滤,专用行各自的白名单照常生效。
|
|
104
|
+
通用行只能有一个 filter,所以用 `deny` 列全敏感工具(数据管线 + Dataset 系列 + 出图 +
|
|
105
|
+
专用角色创建工具);`deny` 里的名字同样必须真实注册。回归断言见
|
|
106
|
+
`test/persona.test.mjs` 的「通用 subagent 行」用例。
|
|
107
|
+
- **同一份 `deny` 也覆盖出网(2026-09-24)**:本插件 14 个检索 / 来源 / 文档解析工具全部列进来,否则只带
|
|
108
|
+
`parentSession` 的通用 child 能自己抓网页,「外部检索只经 web_retriever」又是一条 persona 文案。
|
|
109
|
+
名字的唯一事实来源是 `src/agents/root-tool-policy.ts` 的 `RETRIEVAL_DENIED_TOOLS`(根 Agent 逐名
|
|
110
|
+
deny 用的就是它),测试把两处对齐成同一份名单。**`ocr` 是根侧收敛的唯一例外(2026-09-26)**:它
|
|
111
|
+
不进 `RETRIEVAL_DENIED_TOOLS`(主 Agent 要能解析用户 `@` 进来的本地文档),改由调用级 guard 只拒
|
|
112
|
+
`url` 形态;但通用 child 的这份 `deny` 里它**必须在**——连本地形态一起拿掉,否则通用 child
|
|
113
|
+
继承它能自己解析文档(见 §4.3 → `docs/dev/web-retriever.md`)。**宿主自己挂的 `web_search` / `web_fetch` 绝不能
|
|
114
|
+
写进这里的 `deny`**:它们不由本插件注册,未注册名字在 `deny` 里的表现是"创建子 Agent 失败";
|
|
115
|
+
它们只出现在根侧 `ROOT_AGENT_DENIED_TOOLS`(逐名 `restrict` 的 try/catch 容忍"这个 scope 里没有")。
|
|
116
|
+
- 不用 `dsh-time-context` 行:它每个 step 自动注入时钟读数,与自研 `get_local_datetime`
|
|
117
|
+
重复;时间读数只来自后者(会话组作用域内注册,主 Agent 与全部子 Agent 共享,支持
|
|
118
|
+
`timezone` 换算)。
|
|
119
|
+
- `data_analyst` 行保持 `disabled`。启用前置条件(缺一不可):宿主先实现 Python runner 与
|
|
120
|
+
coding 工具;插件注册 `read_profile`(allow 里的名字必须先真实存在);更新 `AGENTS.md` 与 `docs/dev/data-roles.md`
|
|
121
|
+
的角色边界与 `test/persona.test.mjs` 的预留期断言。
|
|
122
|
+
|
|
123
|
+
## 5.1 bash:为什么只给 data_junior,以及它到底能做什么
|
|
124
|
+
|
|
125
|
+
- **动机**:`query_dataset` 只有 `count/min/max/avg/sum`,变化量 / 增幅 / CAGR / 比值这类派生
|
|
126
|
+
指标没有宿主工具;模型只能拿 `time_facts.first/last` 心算——那正是 §1.3 那次 41,961 字符
|
|
127
|
+
时间戳心算的同型浪费。所以给 data_junior 一个纯计算兜底。**不是**给它开取数通道。
|
|
128
|
+
- **为什么主 Agent 不给**:主 Agent 今天没有任何通用执行 / 文件 / 网络工具(Web 平面把 host 层
|
|
129
|
+
的 `tool-bash` / `tool-fs` / `tool-web` 都 `disabled` 了,本 preset 也不挂回来)。这是一条
|
|
130
|
+
结构性事实,不是 persona 约定:主 Agent 唯一对用户说话、又会读到子 Agent 回传(含网页材料),
|
|
131
|
+
一旦拥有 bash 就等于拿到"注入 → 执行 → 读凭据 → 出网"的完整链路。收敛靠
|
|
132
|
+
`ROOT_AGENT_DENIED_TOOLS`(含 `bash` / `pwsh`)。
|
|
133
|
+
- **平台成对挂载**:Windows 上 host 层挂的是 `pwsh-sandbox`,没有 `bash-sandbox`;只挂
|
|
134
|
+
`tool-bash` 会让名为 bash 的工具实际执行 PowerShell。所以两行互补 `disabled`,allow / deny
|
|
135
|
+
里的名字用 `!!js` 三元表达式按平台选。`tools.restrict()` 对未注册名字报错,所以表达式与行必须
|
|
136
|
+
同进同退(回归:`test/persona.test.mjs`「shell 行:平台成对挂载……」)。
|
|
137
|
+
- **不给后台执行**:`enableRunInBackground: false`。本 preset 不挂 `tool-jobs`,开着后台只会让
|
|
138
|
+
模型拿到一个永远读不回来的 jobId(`job_output` / `job_kill` 不在任何 allow 里)。
|
|
139
|
+
- **闸门只有两层,且都不是安全边界**(`src/agents/bash-guard.ts`):
|
|
140
|
+
① 只对**被委派**子会话开放(镜像 `tool-exec.ts` 的 `delegatedSession`,执行层兜住可见性被改坏);
|
|
141
|
+
② 拦掉 `sandbox_permissions`——委派会话的审批策略由框架固定为 `never`,升级必然失败,而且模型
|
|
142
|
+
会收到「the user rejected…」这种把系统拒绝说成用户拒绝的文案。
|
|
143
|
+
**刻意不解析 `command`**:本仓实测沙箱只拦写、不拦读(`~/.dsh/.credentials.yaml` 与
|
|
144
|
+
`sessions/**` 整机可读)、不拦网络(`curl` 直连 200),而正则挡不住 `node -e` / `python3 -c`
|
|
145
|
+
——那正是引入 bash 的目的。禁区与出网纪律是 data_junior persona 的 `# BASH DISCIPLINE` 软约束。
|
|
146
|
+
- **写边界收不窄**:`store.writeContext()` 用**调用方 session** 的 policy 且要求
|
|
147
|
+
`mode ∈ {workspace-write, danger-full-access}`,而 `describe_dataset` 会用 data_junior 的会话写
|
|
148
|
+
`profile.json`;把它的会话钉成 `read-only` 会直接打断 describe_dataset。bash 与 Dataset 管线
|
|
149
|
+
共用同一把尺子(session policy)。
|
|
150
|
+
- **要真正收紧**只有两条路:换掉 `ctx.shell`(容器 / 微 VM / 远端执行器),或给 dsh 进程做出网
|
|
151
|
+
限制。两者都不是 `ctx.sandbox` 能提供的。
|
|
152
|
+
|
|
153
|
+
## 6. 改动流程
|
|
154
|
+
|
|
155
|
+
1. 先判断内容属于"每轮硬规则"(→ persona)还是"按需协议"(→ `skills/`)。
|
|
156
|
+
2. 专用角色六处齐改(判据与顺序见 `docs/dev/preset-persona.md` §8.5):委派行(provider /
|
|
157
|
+
toolName / backgroundMode / persona / toolFilter,allow 必须含 `skill`)、子 persona(骨架 +
|
|
158
|
+
第一条 duty `skill <name>`)、`skills/<name>/SKILL.md`、主 persona 的路由与复用规则、
|
|
159
|
+
`docs/dev/data-roles.md` §1.1 的角色边界、测试断言。
|
|
160
|
+
3. `npm test` 全绿 + `agentPresets.resolve('capital-generation')` 结果里没有 `broken`(挂载成功),
|
|
161
|
+
再开一个 Capital 会话确认工具表、首轮预检,以及子 Agent 是否真的按第一条 duty 先加载 skill。
|