@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +794 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -2
  4. package/assets/cg-icon.png +0 -0
  5. package/capital-config/client.js +28 -0
  6. package/capital-config/client.src.cjs +388 -0
  7. package/capital-config/index.js +113 -0
  8. package/capital-config/package.json +36 -0
  9. package/capital-watchlist/client.js +182 -0
  10. package/capital-watchlist/client.src.cjs +1229 -0
  11. package/capital-watchlist/index.js +772 -0
  12. package/capital-watchlist/package.json +37 -0
  13. package/chart-ui/README.md +112 -0
  14. package/chart-ui/client.js +16 -0
  15. package/chart-ui/client.src.cjs +126 -0
  16. package/chart-ui/index.js +205 -0
  17. package/chart-ui/package.json +30 -0
  18. package/cordis.patch.yml +72 -0
  19. package/lib/agents/bash-guard.d.ts +55 -0
  20. package/lib/agents/bash-guard.js +162 -0
  21. package/lib/agents/root-tool-policy.d.ts +164 -0
  22. package/lib/agents/root-tool-policy.js +309 -0
  23. package/lib/chart/artifact-ref.d.ts +65 -0
  24. package/lib/chart/artifact-ref.js +107 -0
  25. package/lib/chart/downsample.d.ts +8 -0
  26. package/lib/chart/downsample.js +53 -0
  27. package/lib/chart/errors.d.ts +16 -0
  28. package/lib/chart/errors.js +15 -0
  29. package/lib/chart/events.d.ts +120 -0
  30. package/lib/chart/events.js +471 -0
  31. package/lib/chart/html.d.ts +21 -0
  32. package/lib/chart/html.js +154 -0
  33. package/lib/chart/runtime.d.ts +20 -0
  34. package/lib/chart/runtime.js +270 -0
  35. package/lib/chart/series.d.ts +102 -0
  36. package/lib/chart/series.js +424 -0
  37. package/lib/chart/source-token.d.ts +64 -0
  38. package/lib/chart/source-token.js +217 -0
  39. package/lib/chart/spec.d.ts +58 -0
  40. package/lib/chart/spec.js +265 -0
  41. package/lib/chart/tool.d.ts +86 -0
  42. package/lib/chart/tool.js +288 -0
  43. package/lib/chart/vendor/LICENSE +201 -0
  44. package/lib/chart/vendor/VERSION.json +8 -0
  45. package/lib/chart/vendor/lightweight-charts.standalone.production.js +7 -0
  46. package/lib/data-collector/args.d.ts +38 -0
  47. package/lib/data-collector/args.js +114 -0
  48. package/lib/data-collector/dataset-tools.d.ts +3 -0
  49. package/lib/data-collector/dataset-tools.js +421 -0
  50. package/lib/data-collector/describe.d.ts +99 -0
  51. package/lib/data-collector/describe.js +256 -0
  52. package/lib/data-collector/hub.d.ts +187 -0
  53. package/lib/data-collector/hub.js +469 -0
  54. package/lib/data-collector/query-time.d.ts +58 -0
  55. package/lib/data-collector/query-time.js +101 -0
  56. package/lib/data-collector/query.d.ts +57 -0
  57. package/lib/data-collector/query.js +473 -0
  58. package/lib/data-collector/store.d.ts +499 -0
  59. package/lib/data-collector/store.js +1486 -0
  60. package/lib/data-collector/time-axis.d.ts +169 -0
  61. package/lib/data-collector/time-axis.js +419 -0
  62. package/lib/data-collector/tools.d.ts +32 -0
  63. package/lib/data-collector/tools.js +216 -0
  64. package/lib/index.d.ts +275 -0
  65. package/lib/index.js +458 -0
  66. package/lib/net/eastmoney-client.d.ts +104 -0
  67. package/lib/net/eastmoney-client.js +118 -0
  68. package/lib/net/throttle.d.ts +16 -0
  69. package/lib/net/throttle.js +34 -0
  70. package/lib/ocr/client.d.ts +82 -0
  71. package/lib/ocr/client.js +281 -0
  72. package/lib/ocr/markdown.d.ts +75 -0
  73. package/lib/ocr/markdown.js +209 -0
  74. package/lib/ocr/store.d.ts +119 -0
  75. package/lib/ocr/store.js +226 -0
  76. package/lib/sources/eastmoney-http.d.ts +2 -0
  77. package/lib/sources/eastmoney-http.js +1253 -0
  78. package/lib/sources/fuyao-core.d.ts +52 -0
  79. package/lib/sources/fuyao-core.js +96 -0
  80. package/lib/sources/fuyao-rest.d.ts +14 -0
  81. package/lib/sources/fuyao-rest.js +2057 -0
  82. package/lib/sources/security-code.d.ts +27 -0
  83. package/lib/sources/security-code.js +104 -0
  84. package/lib/sources/security-identity.d.ts +24 -0
  85. package/lib/sources/security-identity.js +89 -0
  86. package/lib/sources/tencent-http.d.ts +2 -0
  87. package/lib/sources/tencent-http.js +1053 -0
  88. package/lib/sources/wind-mcp.d.ts +8 -0
  89. package/lib/sources/wind-mcp.js +593 -0
  90. package/lib/time/tools.d.ts +74 -0
  91. package/lib/time/tools.js +464 -0
  92. package/lib/tool-exec.d.ts +29 -0
  93. package/lib/tool-exec.js +17 -0
  94. package/lib/web-retriever/engines.d.ts +61 -0
  95. package/lib/web-retriever/engines.js +202 -0
  96. package/lib/web-retriever/html-markdown.d.ts +50 -0
  97. package/lib/web-retriever/html-markdown.js +321 -0
  98. package/lib/web-retriever/local-fetch.d.ts +121 -0
  99. package/lib/web-retriever/local-fetch.js +562 -0
  100. package/lib/web-retriever/retriever.d.ts +58 -0
  101. package/lib/web-retriever/retriever.js +94 -0
  102. package/lib/web-retriever/sources.d.ts +235 -0
  103. package/lib/web-retriever/sources.js +961 -0
  104. package/lib/web-retriever/tools.d.ts +50 -0
  105. package/lib/web-retriever/tools.js +1029 -0
  106. package/lib/web-retriever/wind-client.d.ts +69 -0
  107. package/lib/web-retriever/wind-client.js +311 -0
  108. package/locale/en.json +6 -0
  109. package/locale/zh.json +6 -0
  110. package/package.json +76 -4
  111. package/preset/capital-generation/README.md +161 -0
  112. package/preset/capital-generation/agent.patch.yml +509 -0
  113. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +137 -0
  114. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +416 -0
  115. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +174 -0
  116. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +296 -0
  117. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +240 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `@v587d/capital-charts` browser half.
3
+ *
4
+ * 单一 surface:`tool.call.toolview['render_chart']` —— 图表生成过程的明细卡片
5
+ * (子会话与展开视图里都能看)。数据经 `/capital-charts/<chart_id>.json` 旁路取,
6
+ * 不进任何 Agent 上下文。
7
+ *
8
+ * ⚠️ 这里**没有** turn-tail 卡片,且不要再加回来(2026-09-18 事故,实测根因):
9
+ * 内嵌卡片依赖宿主往会话里追加自定义事件 `capital/chart-rendered`,而会话日志的事件
10
+ * 词汇表是闭集——`Session.append` 无法设置 `ignorable`,非 first-party 事件写进去之后,
11
+ * **整份会话在冷加载时会被 fail-closed 拒绝**(6 份会话 `failed to observe session`)。
12
+ * 现在图表以官方 `deliverables/presented` 登记为「本轮交付」,用户在收尾的交付行点开
13
+ * `chart.html`,由官方 document preview 的 script-enabled iframe 渲染自包含图表
14
+ * (见 src/chart/events.ts)。**呈现走官方通道,客户端不再需要任何图表专属投影。**
15
+ */
16
+ const { useEffect, useRef, useState, createElement: h } = require('react')
17
+ const LightweightCharts = require('lightweight-charts')
18
+ const { __capitalChart } = require('./client.runtime.gen.cjs')
19
+
20
+ const TOOLVIEW_SLOT = 'tool.call.toolview'
21
+
22
+ function readReceipt(block) {
23
+ if (!block || block.kind !== 'tool-result') return undefined
24
+ const items = Array.isArray(block.content) ? block.content : []
25
+ for (const item of items) {
26
+ if (item && item.type === 'text' && typeof item.text === 'string') {
27
+ try {
28
+ const parsed = JSON.parse(item.text)
29
+ if (parsed && typeof parsed === 'object') return parsed
30
+ } catch {
31
+ // Not the structured receipt; continue scanning.
32
+ }
33
+ }
34
+ }
35
+ return undefined
36
+ }
37
+
38
+ function ChartCanvas(props) {
39
+ const holder = useRef(null)
40
+ useEffect(() => {
41
+ const node = holder.current
42
+ if (!node || !props.payload) return undefined
43
+ let chart
44
+ try {
45
+ chart = __capitalChart.render(node, props.payload, LightweightCharts)
46
+ } catch (error) {
47
+ node.textContent = '图表渲染失败:' + String((error && error.message) || error)
48
+ }
49
+ return () => {
50
+ try {
51
+ if (chart && typeof chart.remove === 'function') chart.remove()
52
+ } catch {
53
+ // The host removed the card.
54
+ }
55
+ }
56
+ }, [props.payload])
57
+ return h('div', { ref: holder, 'data-chart-canvas': '1', style: { width: '100%', height: '320px' } })
58
+ }
59
+
60
+ function useJson(url) {
61
+ const [value, setValue] = useState(null)
62
+ const [error, setError] = useState(null)
63
+ useEffect(() => {
64
+ setValue(null)
65
+ setError(null)
66
+ if (!url) return undefined
67
+ const controller = typeof AbortController === 'function' ? new AbortController() : null
68
+ let active = true
69
+ fetch(url, controller ? { cache: 'no-store', signal: controller.signal } : { cache: 'no-store' })
70
+ .then((response) => {
71
+ if (!response.ok) throw new Error('HTTP ' + response.status)
72
+ return response.json()
73
+ })
74
+ .then((next) => {
75
+ if (active) setValue(next)
76
+ })
77
+ .catch((reason) => {
78
+ if (active) setError(String((reason && reason.message) || reason))
79
+ })
80
+ return () => {
81
+ active = false
82
+ if (controller) controller.abort()
83
+ }
84
+ }, [url])
85
+ return { value, error }
86
+ }
87
+
88
+ /** 工具过程明细:保留在子会话与展开视图里。 */
89
+ function ChartView(props) {
90
+ const block = props.block
91
+ const settled = Boolean(block) && block.kind === 'tool-result'
92
+ const failed = settled && block.isError
93
+ const receipt = settled ? readReceipt(block) : undefined
94
+ const url = receipt && typeof receipt.chart_url === 'string' ? receipt.chart_url : null
95
+ const { value: payload, error: loadError } = useJson(url)
96
+
97
+ if (!settled) return h('div', { className: 'capital-chart', 'data-state': 'running' }, '正在生成图表…')
98
+ if (failed || !receipt) return h('div', { className: 'capital-chart', 'data-state': 'error' }, '图表生成失败。')
99
+
100
+ const openHtml = () => {
101
+ if (typeof receipt.html_path === 'string') props.openFile(receipt.html_path)
102
+ }
103
+ const head = h(
104
+ 'div',
105
+ { className: 'capital-chart-head', style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontSize: '12px', color: 'var(--dsw-alias-label-secondary)' } },
106
+ h('span', null, typeof receipt.title === 'string' && receipt.title.length > 0 ? receipt.title : '图表'),
107
+ h('button', { type: 'button', onClick: openHtml, title: '打开自包含图表文件', style: { font: 'inherit', background: 'none', border: 'none', color: 'var(--dsw-alias-link)', cursor: 'pointer', padding: 0 } }, '打开'),
108
+ )
109
+
110
+ let body
111
+ if (url === null) body = h('div', { className: 'capital-chart-note' }, '图表已生成;点“打开”查看可交互文件。')
112
+ else if (loadError !== null) body = h('div', { className: 'capital-chart-note' }, `图表数据取不到(${loadError}),可点“打开”查看自包含文件。`)
113
+ else if (payload === null) body = h('div', { className: 'capital-chart-note', 'data-state': 'loading' }, '正在加载图表数据…')
114
+ else body = h(ChartCanvas, { payload })
115
+ return h('div', { className: 'capital-chart', 'data-state': 'ok', style: { margin: '8px 0' } }, head, body)
116
+ }
117
+
118
+ function apply(ctx) {
119
+ ctx.slots.inject(TOOLVIEW_SLOT, () => ctx.slots.register({ name: TOOLVIEW_SLOT, key: 'render_chart' }, ChartView))
120
+ ctx.logger?.info?.('capital-charts: 客户端半边已挂载(render_chart toolview;呈现走官方交付通道)')
121
+ }
122
+
123
+ exports.name = 'capital-charts'
124
+ exports.inject = ['slots']
125
+ exports.apply = apply
126
+ exports.ChartView = ChartView
@@ -0,0 +1,205 @@
1
+ /**
2
+ * `@v587d/capital-charts` 的 host half。
3
+ *
4
+ * It owns only opaque chart registrations and their browser route. It never
5
+ * reads Dataset files itself; render_chart supplies the resolved chart series
6
+ * path, and the turn event published by the plugin carries the small metadata
7
+ * the client renders.
8
+ */
9
+
10
+ export const name = 'capital-charts'
11
+ export const ROUTE_PATH = '/capital-charts'
12
+
13
+ const CHART_ID_PATTERN = /^ch_[A-Za-z0-9._-]{1,80}$/
14
+ const MAX_ENTRIES = 500
15
+
16
+ function defaultReadFile(path) {
17
+ const processLike = globalThis.process
18
+ const fs = processLike?.getBuiltinModule?.('node:fs')
19
+ if (!fs || typeof fs.readFileSync !== 'function') throw new Error('capital-charts: Node fs is unavailable')
20
+ return fs.readFileSync(path, 'utf8')
21
+ }
22
+
23
+ export function createChartStore(options = {}) {
24
+ const readFile = options.readFile ?? defaultReadFile
25
+ let routeReady = options.routeReady ?? true
26
+ const files = new Map()
27
+
28
+ return {
29
+ publish(input) {
30
+ const chartId = input?.chartId
31
+ const filePath = input?.filePath
32
+ if (typeof chartId !== 'string' || !CHART_ID_PATTERN.test(chartId)) {
33
+ throw new Error(`capital-charts: chart_id is invalid: ${String(chartId)}`)
34
+ }
35
+ if (typeof filePath !== 'string' || filePath.length === 0) {
36
+ throw new Error('capital-charts: filePath is required')
37
+ }
38
+ files.delete(chartId)
39
+ files.set(chartId, filePath)
40
+ while (files.size > MAX_ENTRIES) files.delete(files.keys().next().value)
41
+ return routeReady ? `${ROUTE_PATH}/${chartId}.json` : null
42
+ },
43
+
44
+ setRouteReady(value) {
45
+ routeReady = value === true
46
+ },
47
+
48
+ filePathOf(chartId) {
49
+ return files.get(chartId)
50
+ },
51
+
52
+ read(filePath) {
53
+ return readFile(filePath)
54
+ },
55
+
56
+ get size() {
57
+ return files.size
58
+ },
59
+ }
60
+ }
61
+
62
+ function send(res, status, payload) {
63
+ const body = JSON.stringify(payload)
64
+ res.statusCode = status
65
+ res.setHeader('content-type', 'application/json; charset=utf-8')
66
+ res.setHeader('cache-control', 'no-store')
67
+ res.end(body)
68
+ }
69
+
70
+ function pathnameOf(req, res) {
71
+ try {
72
+ return new URL(req.url ?? '/', 'http://localhost').pathname
73
+ } catch {
74
+ send(res, 400, { error: 'bad_request' })
75
+ return undefined
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Build the series route handler.
81
+ *
82
+ * @param store - the opaque chart registry.
83
+ * @param options.authorize - optional `(req) => 401 | 403 | undefined`. When it
84
+ * returns a status the handler answers with that status and never touches the
85
+ * registry, so an unauthenticated caller learns nothing about `chart_id`.
86
+ * `apply()` passes `connection.requestRejection` here: that is the same
87
+ * trusted-Host fence + signed browser cookie every other session-scoped Web
88
+ * route runs behind (`/api`, the static index), and a route that skips it is
89
+ * readable by any local process or page that learns a `chart_id`
90
+ * (2026-09-17 review). A composition with no `connection` service (no browser
91
+ * auth plane at all) passes nothing and keeps the old behaviour.
92
+ */
93
+ export function createRouteHandler(store, options = {}) {
94
+ const authorize = options.authorize
95
+ return async function handler(req, res) {
96
+ if (typeof authorize === 'function') {
97
+ let rejection
98
+ try {
99
+ rejection = authorize(req)
100
+ } catch {
101
+ // An authorizer that cannot answer must not fall through to the data:
102
+ // fail closed on the same status the platform uses for a missing cookie.
103
+ rejection = 401
104
+ }
105
+ if (rejection !== undefined && rejection !== null) {
106
+ res.statusCode = rejection
107
+ res.setHeader('content-type', 'text/plain; charset=utf-8')
108
+ res.setHeader('cache-control', 'no-store')
109
+ res.end(rejection === 401 ? 'unauthorized' : 'forbidden')
110
+ return
111
+ }
112
+ }
113
+ if (req?.method !== 'GET') {
114
+ send(res, 405, { error: 'method_not_allowed' })
115
+ return
116
+ }
117
+ const pathname = pathnameOf(req, res)
118
+ if (pathname === undefined) return
119
+ if (pathname !== ROUTE_PATH && !pathname.startsWith(`${ROUTE_PATH}/`)) {
120
+ send(res, 404, { error: 'not_found' })
121
+ return
122
+ }
123
+ const match = /^\/([^/]+)\.json$/.exec(pathname.slice(ROUTE_PATH.length))
124
+ if (!match) {
125
+ send(res, 404, { error: 'not_found' })
126
+ return
127
+ }
128
+ let chartId
129
+ try {
130
+ chartId = decodeURIComponent(match[1])
131
+ } catch {
132
+ send(res, 400, { error: 'bad_request' })
133
+ return
134
+ }
135
+ if (!CHART_ID_PATTERN.test(chartId)) {
136
+ send(res, 400, { error: 'invalid_chart_id' })
137
+ return
138
+ }
139
+ const filePath = store.filePathOf(chartId)
140
+ if (filePath === undefined) {
141
+ send(res, 404, { error: 'chart_not_published', chart_id: chartId })
142
+ return
143
+ }
144
+ let body
145
+ try {
146
+ body = await store.read(filePath)
147
+ } catch {
148
+ send(res, 404, { error: 'chart_file_unreadable', chart_id: chartId })
149
+ return
150
+ }
151
+ res.statusCode = 200
152
+ res.setHeader('content-type', 'application/json; charset=utf-8')
153
+ res.setHeader('cache-control', 'no-store')
154
+ res.end(body)
155
+ }
156
+ }
157
+
158
+ export function apply(ctx) {
159
+ const store = createChartStore({ routeReady: false })
160
+ ctx.provide('capitalCharts', {
161
+ publish: (input) => store.publish(input),
162
+ })
163
+
164
+ /**
165
+ * 认证围栏:优先解析 host 平面的 `connection`(web profile 由
166
+ * `@deepseek-ai/dsh-client-connection` 提供),把它的 `requestRejection`
167
+ * 原样接到本路由上——trusted-Host(403)+ 签名 cookie(401)。
168
+ *
169
+ * 惰性解析而不是 apply 时解析一次:`connection` 与本行的挂载先后不由我们决定。
170
+ * 服务整体缺席(Electron/file:// 这类没有浏览器认证面的载体)时返回 undefined,
171
+ * 退化成"没有认证面"的旧行为;但**解析抛错**(上下文已销毁)一律 401,宁可拒绝。
172
+ */
173
+ const authorize = (req) => {
174
+ let connection
175
+ try {
176
+ connection = ctx.get('connection')
177
+ } catch {
178
+ return 401
179
+ }
180
+ if (connection === undefined || connection === null) return undefined
181
+ if (typeof connection.requestRejection !== 'function') return undefined
182
+ return connection.requestRejection(req)
183
+ }
184
+
185
+ const registerRoute = (webCtx) => {
186
+ webCtx.effect(
187
+ () => {
188
+ const chartDispose = webCtx.webServer.register({ kind: 'prefix', path: ROUTE_PATH, handler: createRouteHandler(store, { authorize }) })
189
+ store.setRouteReady(true)
190
+ return () => {
191
+ store.setRouteReady(false)
192
+ if (typeof chartDispose === 'function') chartDispose()
193
+ }
194
+ },
195
+ 'capital-charts: series route',
196
+ )
197
+ }
198
+ // 只走 inject,不许"先 get 探测、再直接拿属性"。`ctx.get('webServer')` 能解析出值
199
+ // **不代表** `ctx.webServer` 这个属性访问合法:cordis 的属性代理只认"本层声明过 inject"
200
+ // (ReflectService 找不到就抛 cannot get property "webServer" without inject)。探测式写法
201
+ // 把正确性押在装配顺序上——桌面端 webServer 在本行之前已就绪,于是走了"直接调用"那条并
202
+ // 当场炸(2026-09-30 实机:capital-charts 与 capital-watchlist 两行 异常);web profile 里
203
+ // 它还没就绪,反而躲过。inject 两种顺序都对:已就绪立刻起,没出现就一直等(本行仍激活)。
204
+ ctx.inject(['webServer'], registerRoute)
205
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@v587d/capital-charts",
3
+ "version": "2.6.0",
4
+ "description": "Capital 模式的图表前端组件(浏览器半边 + 序列旁路路由):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./index.js"
10
+ },
11
+ "./client": {
12
+ "default": "./client.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "client": {
18
+ "platform": "web",
19
+ "immediately": true,
20
+ "inject": [
21
+ "@deepseek-ai/dsh-client-ui-conversation"
22
+ ]
23
+ }
24
+ },
25
+ "files": [
26
+ "index.js",
27
+ "client.js",
28
+ "README.md"
29
+ ]
30
+ }
@@ -0,0 +1,72 @@
1
+ # Capital Generation is a distributable DSH profile bundle.
2
+ #
3
+ # 本文件只挂 **host 平面** 的客户端 bundle 行(下面三个 insert)。预设不在这里:0.1.7 起
4
+ # 预设是普通条目行,本包的预设声明住在 `preset/capital-generation/agent.patch.yml`,由
5
+ # package.json 的 `dsh.bundle.patch` 数组按序应用(`agent-preset-registry` 行由 dsh-web-app
6
+ # 自己提供,我们只 insert 自己的预设声明,不动它的 config)。
7
+ #
8
+ # ⛔ 升级事故(2026-09-26,issue #3):这里原先是 patch `agent-presets` 行 + `config.roots`
9
+ # 的目录式注册。0.1.7 把该条目改名为 `agent-preset-registry` 并**删掉了 roots 机制**(registry
10
+ # 「既不扫目录也不接受预设路径」),于是每次启动静默报
11
+ # `dsh: [@v587d/capital-generation] patch: entry "agent-presets" not found` —— 预设永不注册,
12
+ # 历史会话(头部记着 `"agentPreset":"capital-generation"`)全部 `Unknown agent preset` 读不回。
13
+
14
+ # ── 图表前端组件(host 平面行)────────────────────────────────────────────────
15
+ #
16
+ # 为什么这一行必须在这里、必须是 host 平面:客户端 bundle 的注入发生在
17
+ # index.html 渲染那一刻,运行期新增 entry 没有送达浏览器的通道;挂在 preset 行上的
18
+ # 客户端 bundle 到不了已经打开的页面。见 chart-ui/README.md。
19
+ #
20
+ # 为什么行名是**相对本包目录的路径**(三个坑都踩过,都不是猜的):
21
+ # 1. **`name` 不接受 `!!js`**:loader 只对 fiber 的 `config` 做插值
22
+ # (cordis-plugin-loader 的 `internal/config` 钩子),`name` 永远按字面量送进 ESM import。
23
+ # 写过 !!js 的后果是启动期 `name.startsWith is not a function`——整个 profile 起不来。
24
+ # 2. **`name` 也不能是 subpath 说明符**(如 `@v587d/capital-generation/chart-ui`):
25
+ # dsh-client-modules 的 locatePkgJson 里
26
+ # `if (!pathLike && exactPackageSpecifier(loaderName) === undefined) return undefined`
27
+ # —— subpath 既非 path-like、打包名解析又返回 undefined,于是这颗行被**静默**当成
28
+ # "不是客户端包",bundle 永不进 __DSH_BOOT__(表现是图表 UI 不出现,且不报错)。
29
+ # 3. 解析基址是**本 patch 文件所在目录**,不是 profile 根:
30
+ # cordis-plugin-include 装载 patch 时会 `ctx.baseUrl = new URL('.', pathToFileURL(filename))`。
31
+ # 所以这里的相对路径就是本包目录下的 chart-ui/index.js —— 与 profile 的 node_modules
32
+ # 布局无关(pnpm 的 isolated 布局也照常工作)。
33
+ # locatePkgJson 随后按"最近的 package.json"归属,正好是 chart-ui/package.json,
34
+ # 包身份为 @v587d/capital-charts,与主包不撞名。
35
+ # 回归断言见 test/chart-composition.test.mjs(name 必须是字面量且 path-like,并真的解析一次)。
36
+ - insert:
37
+ - id: capital-config
38
+ name: './capital-config/index.js'
39
+ config:
40
+ customPersona: ''
41
+ fuyaoCredentialRef: FUYAO_API_KEY
42
+ retriever:
43
+ baseURL: ''
44
+ credentialRef: ANYSEARCH_API_KEY
45
+ windDocs:
46
+ endpoint: 'https://mcp.wind.com.cn/vserver_financial_docs/mcp/'
47
+ credentialRef: WIND_API_KEY
48
+ timeoutMs: 60000
49
+
50
+ - insert:
51
+ - id: capital-charts
52
+ name: './chart-ui/index.js'
53
+
54
+ # ── 用户自选股(host 平面行)──────────────────────────────────────────────────
55
+ #
56
+ # 为什么也必须在 host 平面:自选股是**跨 workspace 的用户侧资产**(清单与报价快照落
57
+ # `~/.dsh/storages/capital_watchlist.json`),而客户端 bundle 的注入只发生在 index.html
58
+ # 渲染那一刻——挂在 preset 行上就到不了已经打开的页面(同 chart-ui 的理由)。
59
+ #
60
+ # 与上一颗 preset 行的方向是**反的**:chart-ui 的数据由插件侧 push 进来(`ctx.get` 只能
61
+ # 从会话作用域向外解析),本行则要在 host 平面自己出网,所以它 import
62
+ # `../lib/sources/fuyao-core.js`(零 import 的叶子内核)——**同一份** key 解析 / 信封判定 /
63
+ # 错误映射,不许有第二份(AGENTS.md §9.7)。这条跨包相对导入在本仓没有先例,因此由
64
+ # `test/watchlist-quote.test.mjs` 的**模块同一性探针**钉住:两处 import 必须拿到同一实例,
65
+ # 探针不过就重新设计,而不是先上线再说。
66
+ #
67
+ # `id` 与 `./capital-watchlist/index.js` 的 path-like 字面量规则同上(!!js / subpath 两个坑
68
+ # 对这颗行同样成立)。
69
+ - insert:
70
+ - id: capital-watchlist
71
+ name: './capital-watchlist/index.js'
72
+
@@ -0,0 +1,55 @@
1
+ /**
2
+ * data_junior 的 shell 调用闸门(`bash` / `pwsh` 两个名字都认,两层,都是**可靠判定**,不看命令内容)。
3
+ *
4
+ * 背景与边界(设计理由全文在 preset/capital-generation/README.md):
5
+ * - bash 不是计算器,是一台完整终端。本仓实测:沙箱**只拦写**(workspace + /tmp 之外
6
+ * 一律 EROFS),**不拦读**(`~/.dsh/.credentials.yaml`、`sessions/**` 整机可读),
7
+ * **不拦网络**(`curl` 直连 200)。这是 DSH 沙箱词汇表的既定边界——`SandboxMode`
8
+ * 只覆盖文件效果,官方文档亦如此声明。
9
+ * - 因此本文件**不做命令内容匹配**:正则挡不住 `node -e`/`python3 -c`,而 node/python
10
+ * 正是引入 bash 的目的(未来 data_analyst 也靠它)。禁区与出网纪律走 data_junior 的
11
+ * persona 软约束,不在这里伪装成安全边界。
12
+ * - 沙箱写边界也无法单独收窄:数据集管线自己就走沙箱,`store.writeContext()` 用**调用方
13
+ * session** 的 policy 且要求 mode ∈ {workspace-write, danger-full-access},把 data_junior
14
+ * 的会话钉成 read-only 会直接打断 `describe_dataset` 的 profile 落盘
15
+ * (`workspace_not_writable`)。bash 与 Dataset 管线共用同一把尺子(session policy)。
16
+ *
17
+ * 两层规则:
18
+ * A 结构层:shell 只对**被委派**的子会话开放(镜像 src/tool-exec.ts 的 delegatedSession)——
19
+ * 即使哪天 ROOT_AGENT_DENIED_TOOLS 或 allow 过滤被改坏,执行层仍然拒绝根会话。
20
+ * docs/dev/bash-gate.md「工具可见性不是权限隔离」。
21
+ * B UX 层:委派子会话的审批策略由框架固定为 `never`(dsh-subagent 的
22
+ * `captureDelegatedPolicyOverrides` → `approval/policy: never`,`dsh-user-approval.decide()`
23
+ * 在 never 时**直接返回 rejected、根本不调用 answerer**),所以 `sandbox_permissions`
24
+ * 必然失败,而且模型会收到「the user rejected…」这种**把系统拒绝说成用户拒绝**的文案。
25
+ * 这里提前拦下并给出正确路径:省一次必然白跑的往返 + 纠正归因。
26
+ *
27
+ * 为什么用 `tools.guard` 而不是 `tools/pre-execute`:guard 是**单调**的(没有 allow 结果,
28
+ * 任何监听顺序都无法把别人的 deny 翻回 allow),同步、拿到解析后的 `arguments`,且
29
+ * 「注册在 agent.ctx 上就只作用于该 agent」。pre-execute 是 waterfall,可扩展也意味着可被后续
30
+ * 监听改写。
31
+ */
32
+ import { type GuardedExecution, type PolicyContext } from './root-tool-policy.js';
33
+ /** 闸门看到的调用形状:guard 拿到的是解析后的 arguments,不是一个 shell 字符串。 */
34
+ export type BashGuardExecution = GuardedExecution;
35
+ /** A 结构层拒绝文案:模型可据此改走委派。 */
36
+ export declare const SHELL_DELEGATED_ONLY: string;
37
+ /** B UX 层拒绝文案:说清"这不是用户拒绝",并给出正确路径。 */
38
+ export declare const SHELL_ESCALATION_UNREACHABLE: string;
39
+ /**
40
+ * 单次 shell 调用的判定:返回拒绝理由,或 undefined 放行。
41
+ *
42
+ * 判定顺序是"先身份、后能力":身份不对时不该让模型以为问题出在参数上。
43
+ * 注意这里**只读两个字段**(调用方身份、`sandbox_permissions`/`justification`),
44
+ * 绝不解析 `command`——理由见文件头。
45
+ */
46
+ export declare function bashGuardReason(exec: BashGuardExecution | undefined): string | undefined;
47
+ /**
48
+ * 注册 bash 闸门:对**本 preset 的每个 agent** 在它自己的 scope 上装一次 guard。
49
+ *
50
+ * 监听位置与 `registerRootToolPolicy` 同因:`agent/created` 的路由键是 agent 对象本身,
51
+ * 注册在 standing scope(打了标签)的监听器**收不到且不报错**,必须挂 `ctx.root`。
52
+ * 安装/释放都不得抛回 agent 创建路径——宁可少一层闸门,也不能挡住建 agent;
53
+ * 静默失败会留下诊断 warn(见 `registerBashGuard` 的返回值处理)。
54
+ */
55
+ export declare function registerBashGuard(ctx: PolicyContext): void;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * data_junior 的 shell 调用闸门(`bash` / `pwsh` 两个名字都认,两层,都是**可靠判定**,不看命令内容)。
3
+ *
4
+ * 背景与边界(设计理由全文在 preset/capital-generation/README.md):
5
+ * - bash 不是计算器,是一台完整终端。本仓实测:沙箱**只拦写**(workspace + /tmp 之外
6
+ * 一律 EROFS),**不拦读**(`~/.dsh/.credentials.yaml`、`sessions/**` 整机可读),
7
+ * **不拦网络**(`curl` 直连 200)。这是 DSH 沙箱词汇表的既定边界——`SandboxMode`
8
+ * 只覆盖文件效果,官方文档亦如此声明。
9
+ * - 因此本文件**不做命令内容匹配**:正则挡不住 `node -e`/`python3 -c`,而 node/python
10
+ * 正是引入 bash 的目的(未来 data_analyst 也靠它)。禁区与出网纪律走 data_junior 的
11
+ * persona 软约束,不在这里伪装成安全边界。
12
+ * - 沙箱写边界也无法单独收窄:数据集管线自己就走沙箱,`store.writeContext()` 用**调用方
13
+ * session** 的 policy 且要求 mode ∈ {workspace-write, danger-full-access},把 data_junior
14
+ * 的会话钉成 read-only 会直接打断 `describe_dataset` 的 profile 落盘
15
+ * (`workspace_not_writable`)。bash 与 Dataset 管线共用同一把尺子(session policy)。
16
+ *
17
+ * 两层规则:
18
+ * A 结构层:shell 只对**被委派**的子会话开放(镜像 src/tool-exec.ts 的 delegatedSession)——
19
+ * 即使哪天 ROOT_AGENT_DENIED_TOOLS 或 allow 过滤被改坏,执行层仍然拒绝根会话。
20
+ * docs/dev/bash-gate.md「工具可见性不是权限隔离」。
21
+ * B UX 层:委派子会话的审批策略由框架固定为 `never`(dsh-subagent 的
22
+ * `captureDelegatedPolicyOverrides` → `approval/policy: never`,`dsh-user-approval.decide()`
23
+ * 在 never 时**直接返回 rejected、根本不调用 answerer**),所以 `sandbox_permissions`
24
+ * 必然失败,而且模型会收到「the user rejected…」这种**把系统拒绝说成用户拒绝**的文案。
25
+ * 这里提前拦下并给出正确路径:省一次必然白跑的往返 + 纠正归因。
26
+ *
27
+ * 为什么用 `tools.guard` 而不是 `tools/pre-execute`:guard 是**单调**的(没有 allow 结果,
28
+ * 任何监听顺序都无法把别人的 deny 翻回 allow),同步、拿到解析后的 `arguments`,且
29
+ * 「注册在 agent.ctx 上就只作用于该 agent」。pre-execute 是 waterfall,可扩展也意味着可被后续
30
+ * 监听改写。
31
+ */
32
+ import { agentSessionId, isCapitalAgent, SHELL_TOOL_NAMES } from './root-tool-policy.js';
33
+ /**
34
+ * shell 工具名全集(`bash` / `pwsh`,定义只在 {@link SHELL_TOOL_NAMES} 一份)。
35
+ *
36
+ * ⛔ 这里不能按 `process.platform` 挑一个:preset 里那两行是平台成对挂载的,Windows 上
37
+ * 模型调的是 `pwsh`,而闸门跑在**同一个进程**里——只认 `bash` 的话,两层判定在 Windows 上
38
+ * 一次都不触发,且不报错(2026-09-30 实测发现,症状是"§1.6 那道闸门在 Windows 上是死代码")。
39
+ */
40
+ const shellToolNames = new Set(SHELL_TOOL_NAMES);
41
+ /** A 结构层拒绝文案:模型可据此改走委派。 */
42
+ export const SHELL_DELEGATED_ONLY = 'shell 工具(bash / pwsh)只对被委派的数据子 Agent 开放,当前调用方不是被委派的子会话;'
43
+ + '数据与统计请由主 Agent 委派 data_collector / data_junior。';
44
+ /** B UX 层拒绝文案:说清"这不是用户拒绝",并给出正确路径。 */
45
+ export const SHELL_ESCALATION_UNREACHABLE = 'shell 工具(bash / pwsh)的沙箱升级在子 Agent 会话里永远无法获批(框架把委派会话的审批策略固定为 never,不会去问用户),不要重试:'
46
+ + '缺数值用 describe_dataset / query_dataset,缺数据用 send_message 发 data_gap 回主 Agent。';
47
+ function isRecord(value) {
48
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
49
+ }
50
+ /**
51
+ * 单次 shell 调用的判定:返回拒绝理由,或 undefined 放行。
52
+ *
53
+ * 判定顺序是"先身份、后能力":身份不对时不该让模型以为问题出在参数上。
54
+ * 注意这里**只读两个字段**(调用方身份、`sandbox_permissions`/`justification`),
55
+ * 绝不解析 `command`——理由见文件头。
56
+ */
57
+ export function bashGuardReason(exec) {
58
+ if (exec === undefined || typeof exec.name !== 'string' || !shellToolNames.has(exec.name))
59
+ return undefined;
60
+ const parent = exec.agent?.session?.header?.parentSession;
61
+ if (typeof parent !== 'string' || parent.length === 0)
62
+ return SHELL_DELEGATED_ONLY;
63
+ const args = isRecord(exec.arguments) ? exec.arguments : undefined;
64
+ if (args !== undefined && (args.sandbox_permissions !== undefined || args.justification !== undefined)) {
65
+ return SHELL_ESCALATION_UNREACHABLE;
66
+ }
67
+ return undefined;
68
+ }
69
+ /** 在某个 agent 自己的 scope 上安装 guard,返回 disposer(不可用时返回 undefined)。 */
70
+ function installAgentGuard(agent) {
71
+ let tools;
72
+ try {
73
+ tools = agent?.ctx?.tools;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ if (tools === undefined || typeof tools.guard !== 'function')
79
+ return undefined;
80
+ try {
81
+ const dispose = tools.guard(bashGuardReason);
82
+ return typeof dispose === 'function' ? dispose : undefined;
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
88
+ /**
89
+ * 注册 bash 闸门:对**本 preset 的每个 agent** 在它自己的 scope 上装一次 guard。
90
+ *
91
+ * 监听位置与 `registerRootToolPolicy` 同因:`agent/created` 的路由键是 agent 对象本身,
92
+ * 注册在 standing scope(打了标签)的监听器**收不到且不报错**,必须挂 `ctx.root`。
93
+ * 安装/释放都不得抛回 agent 创建路径——宁可少一层闸门,也不能挡住建 agent;
94
+ * 静默失败会留下诊断 warn(见 `registerBashGuard` 的返回值处理)。
95
+ */
96
+ export function registerBashGuard(ctx) {
97
+ const listenCtx = ctx.root ?? ctx;
98
+ if (typeof listenCtx.on !== 'function')
99
+ return;
100
+ /** agent 会话 id → guard 的 disposer;agent/disposed 时释放,避免跨实例残留。 */
101
+ const guards = new Map();
102
+ const install = () => {
103
+ const stopCreated = listenCtx.on?.('agent/created', (payload) => {
104
+ try {
105
+ const agent = payload?.agent;
106
+ if (!isCapitalAgent(ctx, agent))
107
+ return;
108
+ const dispose = installAgentGuard(agent);
109
+ if (dispose === undefined) {
110
+ ctx.logger?.warn('capital-generation: bash 闸门未生效(agent scope 上没有 tools.guard):data_junior 的 bash 只剩 persona 软约束');
111
+ return;
112
+ }
113
+ const id = agentSessionId(agent);
114
+ if (id !== undefined)
115
+ guards.set(id, dispose);
116
+ }
117
+ catch (error) {
118
+ ctx.logger?.warn(`capital-generation: bash 闸门安装异常:${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ });
121
+ const stopDisposed = listenCtx.on?.('agent/disposed', (payload) => {
122
+ try {
123
+ const id = agentSessionId(payload?.agent);
124
+ if (id === undefined)
125
+ return;
126
+ const dispose = guards.get(id);
127
+ if (dispose === undefined)
128
+ return;
129
+ guards.delete(id);
130
+ dispose();
131
+ }
132
+ catch { /* 释放失败不该影响 agent 拆除路径 */ }
133
+ });
134
+ return () => {
135
+ try {
136
+ if (typeof stopCreated === 'function')
137
+ stopCreated();
138
+ }
139
+ catch { /* ignore */ }
140
+ try {
141
+ if (typeof stopDisposed === 'function')
142
+ stopDisposed();
143
+ }
144
+ catch { /* ignore */ }
145
+ for (const dispose of guards.values()) {
146
+ try {
147
+ dispose();
148
+ }
149
+ catch { /* ignore */ }
150
+ }
151
+ guards.clear();
152
+ };
153
+ };
154
+ if (typeof ctx.effect === 'function') {
155
+ ctx.effect(() => {
156
+ const dispose = install();
157
+ return () => dispose();
158
+ }, 'capital-generation.bash-guard()');
159
+ return;
160
+ }
161
+ install();
162
+ }