@v587d/capital-generation 2.15.1 → 2.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +7 -7
  3. package/automation/builtins.js +3 -3
  4. package/automation/client.js +47 -17
  5. package/automation/client.src.cjs +294 -83
  6. package/automation/contract.js +4 -0
  7. package/automation/defaults.js +2 -0
  8. package/automation/index.js +11 -7
  9. package/automation/package.json +2 -1
  10. package/automation/route.js +8 -1
  11. package/automation/runner.js +28 -6
  12. package/automation/service.js +89 -0
  13. package/automation/settings.js +6 -0
  14. package/automation/workspaces.js +113 -13
  15. package/chart-ui/package.json +1 -1
  16. package/config/client.js +12 -5
  17. package/config/client.src.cjs +223 -0
  18. package/config/index.js +2 -0
  19. package/config/package.json +1 -1
  20. package/lib/agents/root-tool-policy.d.ts +3 -3
  21. package/lib/agents/root-tool-policy.js +4 -2
  22. package/lib/data-collector/dataset-tools.js +22 -0
  23. package/lib/data-collector/describe.js +1 -0
  24. package/lib/data-collector/hub.js +5 -0
  25. package/lib/data-collector/store.d.ts +5 -0
  26. package/lib/data-collector/store.js +17 -0
  27. package/lib/data-collector/time-axis.d.ts +15 -0
  28. package/lib/data-collector/time-axis.js +73 -0
  29. package/lib/data-collector/tools.d.ts +1 -1
  30. package/lib/data-collector/tools.js +12 -1
  31. package/lib/index.d.ts +5 -0
  32. package/lib/index.js +2 -0
  33. package/lib/sources/tencent-http.js +7 -2
  34. package/lib/sources/tencent-public-core.d.ts +2 -0
  35. package/lib/sources/tencent-public-core.js +5 -0
  36. package/lib/sources/wind-mcp.js +3 -3
  37. package/lib/sources-entry.d.ts +1 -1
  38. package/lib/web-retriever/local-fetch.d.ts +1 -1
  39. package/lib/web-retriever/local-fetch.js +1 -1
  40. package/lib/web-retriever/sources.d.ts +59 -0
  41. package/lib/web-retriever/sources.js +294 -0
  42. package/lib/web-retriever/tools.js +202 -9
  43. package/lib/web-retriever/wind-client.d.ts +1 -1
  44. package/lib/web-retriever/wind-client.js +1 -1
  45. package/package.json +1 -1
  46. package/preset/capital-generation/agent.patch.yml +20 -19
  47. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
  48. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
  49. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +24 -13
  50. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +39 -16
  51. package/watchlist/package.json +1 -1
@@ -9,6 +9,8 @@ const {
9
9
  Switch,
10
10
  Tag,
11
11
  Modal,
12
+ Menu,
13
+ IconChevronDownOutlineRegular,
12
14
  IconRightUpOutlineRegular,
13
15
  } = require('@deepseek-ai/dsh-client-ui-primitives')
14
16
  const { SELECTED_SKILL_CATALOG, SELECTED_SKILL_TAG_LABELS } = require('../selected-skills/catalog.js')
@@ -25,6 +27,7 @@ const {
25
27
  SELECTED_SKILL_PRESET_ORDER,
26
28
  SELECTED_SKILL_ROUTING,
27
29
  } = require('../selected-skills/routing.js')
30
+ const { WORKSPACES_ROUTE } = require('../automation/contract.js')
28
31
 
29
32
  /**
30
33
  * Two different strings: `ENTRY_ID` is the settings namespace (= profile entry
@@ -64,6 +67,8 @@ const DOC_URLS = {
64
67
  /** Render order = array order; one list drives status, refs, drafts and event refresh. */
65
68
  const CREDENTIAL_FIELDS = ['fuyao', 'ftshare', 'anysearch', 'wind', 'paddleocr']
66
69
  const AUTOMATION_FIELDS = ['tickMs', 'runTimeoutMinutes', 'idleTimeoutMinutes', 'maxSteps', 'maxToolCalls', 'inAppNotifications']
70
+ const AUTOMATION_WORKSPACE_FIELD = 'defaultWorkspaceId'
71
+ const WORKSPACE_DEDICATED_MENU_ID = '__capgen_dedicated_workspace__'
67
72
  const AUTOMATION_DEFAULTS = {
68
73
  tickMs: 60_000,
69
74
  runTimeoutMinutes: 60,
@@ -125,6 +130,12 @@ const zh = {
125
130
  automationMaxSteps: '最大步骤',
126
131
  automationMaxToolCalls: '最大工具调用',
127
132
  automationInApp: '写入应用内通知',
133
+ automationDefaultWorkspace: '默认工作区',
134
+ automationWorkspacePath: '默认位置:{path}',
135
+ automationWorkspaceMissing: '原工作区已被移除,运行会回退到专属默认工作区。',
136
+ automationWorkspaceLoading: '工作区列表读取中…',
137
+ automationWorkspaceUnavailable: '工作区列表暂时读不到;任务仍会使用默认工作区,切回本格会重试。',
138
+ automationWorkspaceFallback: '专属默认工作区',
128
139
  windLabel: 'Wind Alice API Key',
129
140
  windHint: 'Wind 金融信披类文档检索接口。保存后新自动模式会话生效。',
130
141
  paddleocrLabel: 'PaddleOCR 文档解析 Token',
@@ -190,6 +201,12 @@ const en = {
190
201
  automationMaxSteps: 'Max steps',
191
202
  automationMaxToolCalls: 'Max tool calls',
192
203
  automationInApp: 'Write in-app notifications',
204
+ automationDefaultWorkspace: 'Default workspace',
205
+ automationWorkspacePath: 'Location: {path}',
206
+ automationWorkspaceMissing: 'That workspace was removed; runs fall back to the dedicated default workspace.',
207
+ automationWorkspaceLoading: 'Loading workspaces…',
208
+ automationWorkspaceUnavailable: 'The workspace list is unavailable; runs still use the default workspace. Re-entering this tab retries.',
209
+ automationWorkspaceFallback: 'Dedicated default workspace',
193
210
  windLabel: 'Wind Alice API Key',
194
211
  windHint: 'Wind financial disclosure document retrieval interface. Takes effect in new Auto mode sessions.',
195
212
  paddleocrLabel: 'PaddleOCR document token',
@@ -261,10 +278,17 @@ function installStyles() {
261
278
  .capital-config-automation-group { display: flex; flex-direction: column; gap: 8px; }
262
279
  .capital-config-automation-group > .capital-config-group-label { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; line-height: 18px; }
263
280
  .capital-config-number { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
281
+ .capital-config-workspace { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
264
282
  .capital-config-number-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
265
283
  .capital-config-number-head .capital-config-write-failed { font-size: 11px; line-height: 16px; }
266
284
  .capital-config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; }
267
285
  .capital-config-automation input[type="number"] { width: 100%; box-sizing: border-box; padding: 6px 8px; border: .5px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; }
286
+ .capital-config-select { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-width: 0; height: 34px; padding: 0 10px 0 12px; border: 0; border-radius: 10px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; line-height: 20px; text-align: left; cursor: pointer; transition: background .15s, opacity .15s; }
287
+ .capital-config-select:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
288
+ .capital-config-select:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 0; }
289
+ .capital-config-select:disabled { opacity: .5; cursor: not-allowed; }
290
+ .capital-config-selectvalue { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
291
+ .capital-config-select > svg { flex: none; color: var(--dsw-alias-label-tertiary); }
268
292
  .capital-config-selected-mode { padding: 12px 14px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }
269
293
  .capital-config-switch-row { align-items: center; gap: 12px; display: flex; }
270
294
  .capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
@@ -323,6 +347,89 @@ function installStyles() {
323
347
  return () => style.remove()
324
348
  }
325
349
 
350
+ /**
351
+ * Dedicated workspace is the empty value, so the dropdown can keep one "follow default"
352
+ * entry even when the registry list is empty or the saved id has been removed.
353
+ */
354
+ function workspaceProjection(details, rawSelected, state) {
355
+ const selectedId = typeof rawSelected === 'string' ? rawSelected.trim() : ''
356
+ const items = Array.isArray(details?.workspaces) ? details.workspaces : []
357
+ const dedicated = items.find((item) => item?.dedicated === true)
358
+ const dedicatedPath = typeof dedicated?.path === 'string' ? dedicated.path : ''
359
+ const options = [{
360
+ id: '',
361
+ title: typeof dedicated?.title === 'string' ? dedicated.title : '',
362
+ path: typeof dedicated?.displayPath === 'string' ? dedicated.displayPath : '',
363
+ dedicated: true,
364
+ missing: false,
365
+ }]
366
+ for (const item of items) {
367
+ if (item?.dedicated === true || (dedicatedPath !== '' && item?.path === dedicatedPath) || typeof item?.id !== 'string') continue
368
+ options.push({
369
+ id: item.id,
370
+ title: typeof item.title === 'string' ? item.title : item.id,
371
+ path: typeof item.displayPath === 'string' ? item.displayPath : '',
372
+ dedicated: false,
373
+ missing: false,
374
+ })
375
+ }
376
+ const missing = selectedId !== '' && !options.some((option) => option.id === selectedId)
377
+ if (missing) options.push({ id: selectedId, title: '', path: '', dedicated: false, missing: true })
378
+ const current = options.find((option) => option.id === selectedId) ?? options[0]
379
+ return { options, selectedId, current, missing, ...state }
380
+ }
381
+
382
+ /**
383
+ * DSH General-settings style selector: same filled pill + host `Menu` surface as the
384
+ * automation page. Native `<select>` is avoided so light/dark themes follow host tokens.
385
+ */
386
+ function WorkspaceSelect({ value, options, onChange, disabled, label, describedBy }) {
387
+ const [open, setOpen] = useState(false)
388
+ const triggerRef = useRef(null)
389
+ const currentValue = typeof value === 'string' ? value : ''
390
+ const items = options.map((option) => ({
391
+ id: option.id === '' ? WORKSPACE_DEDICATED_MENU_ID : option.id,
392
+ label: option.label,
393
+ }))
394
+ const menuValue = currentValue === '' ? WORKSPACE_DEDICATED_MENU_ID : currentValue
395
+ const current = items.find((item) => item.id === menuValue) ?? items[0]
396
+ useEffect(() => {
397
+ if (disabled === true && open) setOpen(false)
398
+ }, [disabled, open])
399
+ const closeMenu = () => setOpen(false)
400
+ const selectValue = (id) => {
401
+ setOpen(false)
402
+ triggerRef.current?.focus?.({ preventScroll: true })
403
+ const next = id === WORKSPACE_DEDICATED_MENU_ID ? '' : id
404
+ if (next === currentValue) return undefined
405
+ return onChange(next)
406
+ }
407
+ const trigger = h('button', {
408
+ ref: triggerRef,
409
+ type: 'button',
410
+ className: 'capital-config-select',
411
+ disabled,
412
+ 'aria-label': current?.label ? `${label}:${current.label}` : label,
413
+ 'aria-haspopup': 'menu',
414
+ 'aria-expanded': open ? 'true' : 'false',
415
+ 'aria-describedby': describedBy,
416
+ title: current?.label,
417
+ onClick: () => setOpen((currentOpen) => !currentOpen),
418
+ },
419
+ h('span', { className: 'capital-config-selectvalue', title: current?.label }, current?.label ?? ''),
420
+ h(IconChevronDownOutlineRegular, { size: 14 }))
421
+ return h(Menu, {
422
+ open,
423
+ anchor: trigger,
424
+ items,
425
+ selectedId: menuValue,
426
+ onSelect: selectValue,
427
+ onClose: closeMenu,
428
+ align: 'start',
429
+ portal: true,
430
+ })
431
+ }
432
+
326
433
  class CapitalCardController {
327
434
  constructor(scope, ctx) {
328
435
  this.scope = scope
@@ -337,6 +444,11 @@ class CapitalCardController {
337
444
  this.automationWriting = new Set()
338
445
  this.automationFailed = new Set()
339
446
  this.automationInvalid = new Set()
447
+ this.workspaceWriting = false
448
+ this.workspaceFailed = false
449
+ this.workspaceDetails = null
450
+ this.workspacesInFlight = false
451
+ this.workspacesFailed = false
340
452
  // Row count is external: null = unknown, never 0; fetch only after the card is shown.
341
453
  this.bundleRows = null
342
454
  this.rowsRequested = false
@@ -393,6 +505,12 @@ class CapitalCardController {
393
505
  failed: this.automationFailed.has(field),
394
506
  invalid: this.automationInvalid.has(field),
395
507
  }])),
508
+ workspace: workspaceProjection(this.workspaceDetails, snapshot.value?.automation?.[AUTOMATION_WORKSPACE_FIELD], {
509
+ loading: this.workspacesInFlight,
510
+ unavailable: this.workspacesFailed && this.workspaceDetails === null,
511
+ writing: this.workspaceWriting,
512
+ failed: this.workspaceFailed,
513
+ }),
396
514
  // `count === null` means unknown, not zero: the card decides whether to draw it.
397
515
  components: { count: this.bundleRows },
398
516
  }
@@ -582,6 +700,51 @@ class CapitalCardController {
582
700
  this.store.set(this.projection())
583
701
  }
584
702
 
703
+ /**
704
+ * Workspace ids come from the automation route (same auth plane as its page), never from
705
+ * a path in settings. Failures are not cached: re-entering the tab retries.
706
+ */
707
+ async ensureWorkspaces() {
708
+ if (this.workspaceDetails !== null || this.workspacesInFlight) return
709
+ this.workspacesInFlight = true
710
+ this.workspacesFailed = false
711
+ this.store.set(this.projection())
712
+ try {
713
+ if (typeof fetch !== 'function') throw new Error('fetch_unavailable')
714
+ const response = await fetch(WORKSPACES_ROUTE, { method: 'GET', cache: 'no-store', credentials: 'same-origin' })
715
+ if (!response?.ok) throw new Error(`http_${response?.status ?? 0}`)
716
+ const payload = await response.json()
717
+ if (!Array.isArray(payload?.workspaces)) throw new Error('invalid_payload')
718
+ this.workspaceDetails = {
719
+ workspaces: payload.workspaces.filter((item) => item !== null && typeof item === 'object' && typeof item.id === 'string'),
720
+ }
721
+ } catch {
722
+ this.workspacesFailed = true
723
+ }
724
+ this.workspacesInFlight = false
725
+ this.store.set(this.projection())
726
+ }
727
+
728
+ async writeAutomationWorkspace(value) {
729
+ if (this.workspaceWriting) return 'ignored'
730
+ const snapshot = this.scope.getSnapshot()
731
+ if (snapshot.status !== 'ready' || !snapshot.writable) return 'ignored'
732
+ const next = typeof value === 'string' ? value.trim() : ''
733
+ this.workspaceWriting = true
734
+ this.workspaceFailed = false
735
+ this.store.set(this.projection())
736
+ try {
737
+ await this.scope.mutate([{ op: 'set', path: ['automation', AUTOMATION_WORKSPACE_FIELD], value: next }])
738
+ } catch {
739
+ // The landed check below reports the failure.
740
+ }
741
+ this.workspaceWriting = false
742
+ const landed = (this.scope.getSnapshot().value?.automation?.[AUTOMATION_WORKSPACE_FIELD] ?? '') === next
743
+ if (!landed) this.workspaceFailed = true
744
+ this.store.set(this.projection())
745
+ return landed ? 'saved' : 'failed'
746
+ }
747
+
585
748
  inject() {
586
749
  return {
587
750
  hooks: {
@@ -594,6 +757,8 @@ class CapitalCardController {
594
757
  toggleSelectedSkill: (field, value) => this.toggleSelectedSkill(field, value),
595
758
  toggleSelectedSkillsMode: (auto) => this.toggleSelectedSkillsMode(auto),
596
759
  ensureBundleRows: () => this.ensureBundleRows(),
760
+ ensureWorkspaces: () => this.ensureWorkspaces(),
761
+ writeAutomationWorkspace: (value) => this.writeAutomationWorkspace(value),
597
762
  }
598
763
  }
599
764
  }
@@ -802,6 +967,10 @@ function CapitalCard(props) {
802
967
  const [presetFilter, setPresetFilter] = useState(SELECTED_SKILL_FILTER_ALL)
803
968
  // Fetch on mount: tab labels carry numbers; hooks must precede the unavailable return.
804
969
  useEffect(() => { props.ensureBundleRows() }, [])
970
+ // The workspace list lives behind the automation route; fetch when its tab first opens.
971
+ useEffect(() => {
972
+ if (tab === 'automation') props.ensureWorkspaces()
973
+ }, [tab])
805
974
  // Pinned order is snapshotted on tab entry so rows never move under the cursor.
806
975
  const enabledKeys = () => new Set(SELECTED_SKILL_CATALOG
807
976
  .filter((entry) => state.selectedSkills?.[entry.key]?.on === true)
@@ -887,7 +1056,60 @@ function CapitalCard(props) {
887
1056
  h('span', { className: 'capital-config-group-label' }, t(labelKey)),
888
1057
  ...children,
889
1058
  )
1059
+ const workspaceState = state.workspace ?? {
1060
+ options: [],
1061
+ selectedId: '',
1062
+ current: null,
1063
+ missing: false,
1064
+ loading: false,
1065
+ unavailable: false,
1066
+ writing: false,
1067
+ failed: false,
1068
+ }
1069
+ const workspacePathId = 'capital-config-automation-workspace-path'
1070
+ const namedWorkspaceOptions = workspaceState.options.map((option) => ({
1071
+ id: option.id,
1072
+ path: option.path,
1073
+ label: option.id === ''
1074
+ ? (option.title || t('automationWorkspaceFallback'))
1075
+ : option.missing ? t('automationWorkspaceMissing') : option.title,
1076
+ }))
1077
+ const workspaceLabelCounts = new Map()
1078
+ for (const option of namedWorkspaceOptions) {
1079
+ workspaceLabelCounts.set(option.label, (workspaceLabelCounts.get(option.label) ?? 0) + 1)
1080
+ }
1081
+ const workspaceOptions = namedWorkspaceOptions.map((option) => {
1082
+ if (option.id === '' || !option.path || (workspaceLabelCounts.get(option.label) ?? 0) < 2) return option
1083
+ // Two registered workspaces may legitimately share a title; path keeps the menu unambiguous.
1084
+ return { ...option, label: `${option.label} · ${option.path}` }
1085
+ })
1086
+ const workspaceSection = h('div', { className: 'capital-config-workspace' },
1087
+ h('span', { className: 'capital-config-number-head' },
1088
+ h('span', { className: 'capital-config-switch-label' }, t('automationDefaultWorkspace')),
1089
+ workspaceState.failed
1090
+ ? h('span', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed'))
1091
+ : null,
1092
+ ),
1093
+ h(WorkspaceSelect, {
1094
+ value: workspaceState.selectedId,
1095
+ options: workspaceOptions,
1096
+ onChange: (next) => props.writeAutomationWorkspace(next),
1097
+ disabled: disabled || state.saving || workspaceState.writing,
1098
+ label: t('automationDefaultWorkspace'),
1099
+ describedBy: workspaceState.current?.path ? workspacePathId : undefined,
1100
+ }),
1101
+ workspaceState.unavailable
1102
+ ? h('p', { role: 'status', className: 'capital-config-hint' }, t('automationWorkspaceUnavailable'))
1103
+ : workspaceState.loading
1104
+ ? h('p', { className: 'capital-config-hint' }, t('automationWorkspaceLoading'))
1105
+ : workspaceState.missing
1106
+ ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('automationWorkspaceMissing'))
1107
+ : workspaceState.current?.path
1108
+ ? h('p', { id: workspacePathId, className: 'capital-config-hint' }, t('automationWorkspacePath', { path: workspaceState.current.path }))
1109
+ : null,
1110
+ )
890
1111
  const automationSection = h('div', { className: 'capital-config-automation' },
1112
+ workspaceSection,
891
1113
  automationGroup('automationScheduleGroup', [
892
1114
  h('div', { className: 'capital-config-grid' },
893
1115
  automationNumber('tickMs', 'automationTick'),
@@ -1008,6 +1230,7 @@ function CapitalCard(props) {
1008
1230
  setTab(next)
1009
1231
  // Re-read on every switch to this tab: a previous failure retries here.
1010
1232
  if (next === 'components') props.ensureBundleRows()
1233
+ if (next === 'automation') props.ensureWorkspaces()
1011
1234
  // Re-pin only when entering Skills, so the row under the cursor never jumps.
1012
1235
  if (next === 'skills') setPinnedSkills(enabledKeys())
1013
1236
  },
package/config/index.js CHANGED
@@ -83,6 +83,7 @@ export const Config = z.object({
83
83
  maxSteps: z.number().default(300).volatile().description('单次运行最大步骤数(100–2000)'),
84
84
  maxToolCalls: z.number().default(1_200).volatile().description('单次运行最大工具调用数(100–5000)'),
85
85
  inAppNotifications: z.boolean().default(true).volatile().description('是否写入应用内通知'),
86
+ defaultWorkspaceId: z.string().default('').volatile().description('统一默认工作区 id;空 = 专属默认工作区'),
86
87
  }).default({
87
88
  tickMs: 60_000,
88
89
  runTimeoutMinutes: 60,
@@ -90,6 +91,7 @@ export const Config = z.object({
90
91
  maxSteps: 300,
91
92
  maxToolCalls: 1_200,
92
93
  inAppNotifications: true,
94
+ defaultWorkspaceId: '',
93
95
  }).description('投研自动化默认值;任务级覆盖优先'),
94
96
  retriever: z.object({
95
97
  baseURL: z.string().default('').volatile().description('AnySearch API 地址'),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/config",
3
- "version": "2.15.1",
3
+ "version": "2.16.1",
4
4
  "description": "自动模式的取数凭据与偏好配置行:卡片坐在 @v587d/capital-generation 插件主页的「配置」段,点这一行本身没有表单。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,8 +2,8 @@
2
2
  * Root Agent tool convergence on the root's own scope: preset-level deny would also hit
3
3
  * children. Listeners must live on `ctx.root`; tagged standing-scope listeners miss agent/created.
4
4
  */
5
- /** Outbound tools (search 2 + wind 2 + named sources 9); `ocr` is excluded because only its url form is restricted. */
6
- export declare const RETRIEVAL_DENIED_TOOLS: readonly ["anysearch_search", "web_retriever_fetch", "wind_docs_announcements", "wind_docs_news", "cls_telegraph", "wscn_lives", "cninfo_irm", "sseinfo_qa", "eastmoney_724", "eastmoney_stock_news", "eastmoney_reports", "sina_reports", "ths_eps_forecast"];
5
+ /** Outbound tools (search 2 + wind 2 + named sources 11); `ocr` is excluded because only its url form is restricted. */
6
+ export declare const RETRIEVAL_DENIED_TOOLS: readonly ["anysearch_search", "web_retriever_fetch", "wind_docs_announcements", "wind_docs_news", "cls_telegraph", "wscn_lives", "cninfo_irm", "sseinfo_qa", "eastmoney_724", "eastmoney_stock_news", "eastmoney_reports", "eastmoney_announcements", "eastmoney_announcement_body", "sina_reports", "ths_eps_forecast"];
7
7
  export declare const OCR_TOOL_NAME = "ocr";
8
8
  /** Name-level deny cannot express the form distinction; this is a call-level guard. */
9
9
  export declare const OCR_ROOT_URL_DENIED: string;
@@ -12,7 +12,7 @@ export declare function ocrUrlFormRequested(exec: GuardedExecution | undefined):
12
12
  export declare function ocrFormGuardReasonFor(ctx: PolicyContext): (exec: GuardedExecution | undefined) => string | undefined;
13
13
  /** Mounted as a platform pair; recognize both everywhere or the other platform's gate is dead. */
14
14
  export declare const SHELL_TOOL_NAMES: readonly ["bash", "pwsh"];
15
- export declare const ROOT_AGENT_DENIED_TOOLS: readonly ["render_chart", "prepare_chart_source", "anysearch_search", "web_retriever_fetch", "wind_docs_announcements", "wind_docs_news", "cls_telegraph", "wscn_lives", "cninfo_irm", "sseinfo_qa", "eastmoney_724", "eastmoney_stock_news", "eastmoney_reports", "sina_reports", "ths_eps_forecast", "write", "edit"];
15
+ export declare const ROOT_AGENT_DENIED_TOOLS: readonly ["render_chart", "prepare_chart_source", "anysearch_search", "web_retriever_fetch", "wind_docs_announcements", "wind_docs_news", "cls_telegraph", "wscn_lives", "cninfo_irm", "sseinfo_qa", "eastmoney_724", "eastmoney_stock_news", "eastmoney_reports", "eastmoney_announcements", "eastmoney_announcement_body", "sina_reports", "ths_eps_forecast", "write", "edit"];
16
16
  export declare const CAPITAL_PRESET_ID = "capital-generation";
17
17
  interface ToolRestriction {
18
18
  allow?: readonly string[];
@@ -4,12 +4,14 @@
4
4
  */
5
5
  import { CAPITAL_PRESET_IDS, ROOT_PRESET_ID } from '../preset-ids.js';
6
6
  import { readPresetClaim } from '../preset-claim.js';
7
- /** Outbound tools (search 2 + wind 2 + named sources 9); `ocr` is excluded because only its url form is restricted. */
7
+ /** Outbound tools (search 2 + wind 2 + named sources 11); `ocr` is excluded because only its url form is restricted. */
8
8
  export const RETRIEVAL_DENIED_TOOLS = [
9
9
  'anysearch_search', 'web_retriever_fetch',
10
10
  'wind_docs_announcements', 'wind_docs_news',
11
11
  'cls_telegraph', 'wscn_lives', 'cninfo_irm', 'sseinfo_qa',
12
- 'eastmoney_724', 'eastmoney_stock_news', 'eastmoney_reports', 'sina_reports', 'ths_eps_forecast',
12
+ 'eastmoney_724', 'eastmoney_stock_news', 'eastmoney_reports',
13
+ 'eastmoney_announcements', 'eastmoney_announcement_body',
14
+ 'sina_reports', 'ths_eps_forecast',
13
15
  ];
14
16
  export const OCR_TOOL_NAME = 'ocr';
15
17
  /** Name-level deny cannot express the form distinction; this is a call-level guard. */
@@ -144,6 +144,17 @@ const datasetRefProperties = { dataset_id: { type: 'string' },
144
144
  captured_at: { type: 'integer' },
145
145
  retention_until: { type: 'integer' },
146
146
  params_digest: { type: 'string' },
147
+ time_coverage: {
148
+ type: 'object',
149
+ properties: {
150
+ column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
151
+ from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
152
+ to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
153
+ format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
154
+ },
155
+ required: ['column', 'from', 'to', 'format'],
156
+ additionalProperties: false,
157
+ },
147
158
  };
148
159
  const datasetRefWithAccessSchema = jsonObject({
149
160
  ...datasetRefProperties,
@@ -288,6 +299,17 @@ const describeResultSchema = jsonObject({
288
299
  captured_at: { type: 'integer' },
289
300
  retention_until: { type: 'integer' },
290
301
  params_digest: { type: 'string' },
302
+ time_coverage: {
303
+ type: 'object',
304
+ properties: {
305
+ column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
306
+ from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
307
+ to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
308
+ format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
309
+ },
310
+ required: ['column', 'from', 'to', 'format'],
311
+ additionalProperties: false,
312
+ },
291
313
  query_access: jsonObject({
292
314
  readable: { type: 'boolean' },
293
315
  shape: { type: 'string', enum: ['array', 'envelope_item', 'document', 'none'] },
@@ -171,6 +171,7 @@ export async function describeDataset(input) {
171
171
  captured_at: inspection.captured_at,
172
172
  retention_until: inspection.retention_until,
173
173
  params_digest: inspection.params_digest,
174
+ time_coverage: inspection.time_coverage,
174
175
  query_access: inspection.query_access,
175
176
  profile_id: profile.profile_id,
176
177
  profile_ref: profile.artifact_ref,
@@ -1,3 +1,4 @@
1
+ import { summarizeTimeCoverage } from './time-axis.js';
1
2
  /** Normalize an internal data_key token; data_key is host-only and never model-visible. */
2
3
  export function normalizeKeyToken(value) {
3
4
  return value.replace(/[\\/:]+/g, '.')
@@ -320,6 +321,9 @@ export class DataCollectorHub {
320
321
  throw new Error(`data source ${source.schema.name} returned data incompatible with its output contract`);
321
322
  }
322
323
  const { format, rowCount, rowKey } = shapeOf(output.data, source.schema.rowShape);
324
+ const rows = Array.isArray(output.data)
325
+ ? output.data
326
+ : (rowKey === undefined ? [] : rowsAtPath(output.data, rowKey) ?? []);
323
327
  const ref = await this.store.save({
324
328
  session: item.request.session,
325
329
  capability: item.request.capability,
@@ -331,6 +335,7 @@ export class DataCollectorHub {
331
335
  row_count: rowCount,
332
336
  data: output.data,
333
337
  row_key: rowKey,
338
+ ...(format === 'json_rows' ? { time_coverage: summarizeTimeCoverage(rows) } : {}),
334
339
  });
335
340
  for (const waiter of [...item.waiters])
336
341
  this.settleWaiter(item, waiter, { kind: 'ok', ref });
@@ -1,4 +1,5 @@
1
1
  import { type QueryResult, type QuerySpec } from './query.js';
2
+ import { type DatasetTimeCoverage } from './time-axis.js';
2
3
  /**
3
4
  * Workspace-local Dataset/profile store: immutable artifacts, host-written only. Agents receive
4
5
  * dataset_id / opaque artifact_ref, never real paths; raw rows never enter agent messages.
@@ -38,6 +39,8 @@ export interface DatasetRef {
38
39
  captured_at: number;
39
40
  retention_until: number;
40
41
  params_digest: string;
42
+ /** Host-computed from the stored rows; omitted only by manifests written before this field existed. */
43
+ time_coverage?: DatasetTimeCoverage;
41
44
  }
42
45
  export type DatasetShape = 'array' | 'envelope_item' | 'document' | 'none';
43
46
  export interface DatasetInspection extends DatasetRef {
@@ -293,6 +296,8 @@ export interface SaveDatasetInput {
293
296
  data: unknown;
294
297
  /** Host-only row key for envelope-shaped json_rows data. */
295
298
  row_key?: string;
299
+ /** Computed by the Hub from the same rows it is about to persist. */
300
+ time_coverage?: DatasetTimeCoverage;
296
301
  signal?: AbortSignal;
297
302
  }
298
303
  export interface WriteProfileInput {
@@ -140,6 +140,7 @@ export class WorkspaceDatasetStore {
140
140
  captured_at: capturedAt,
141
141
  retention_until: capturedAt + this.retentionMs,
142
142
  params_digest: input.params_digest,
143
+ ...(input.time_coverage === undefined ? {} : { time_coverage: input.time_coverage }),
143
144
  };
144
145
  const storedManifest = {
145
146
  ...manifest,
@@ -1103,6 +1104,19 @@ function profileQuantile(sorted, probability) {
1103
1104
  return sorted[lower];
1104
1105
  return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
1105
1106
  }
1107
+ function validTimeCoverage(value) {
1108
+ if (!isRecord(value))
1109
+ return false;
1110
+ if (Object.keys(value).some((key) => !['column', 'from', 'to', 'format'].includes(key)))
1111
+ return false;
1112
+ if (!['epoch_ms', 'date_string', 'datetime_string', 'none'].includes(String(value.format)))
1113
+ return false;
1114
+ if (value.format === 'none')
1115
+ return value.column === null && value.from === null && value.to === null;
1116
+ return typeof value.column === 'string' && value.column.length > 0 && value.column.length <= 128
1117
+ && typeof value.from === 'string' && value.from.length > 0
1118
+ && typeof value.to === 'string' && value.to.length > 0;
1119
+ }
1106
1120
  function validateStoredDatasetManifest(value, datasetId) {
1107
1121
  if (!isRecord(value))
1108
1122
  return undefined;
@@ -1130,6 +1144,8 @@ function validateStoredDatasetManifest(value, datasetId) {
1130
1144
  return undefined;
1131
1145
  if (value.row_key !== undefined && (typeof value.row_key !== 'string' || value.row_key.length === 0 || value.row_key.length > 128))
1132
1146
  return undefined;
1147
+ if (value.time_coverage !== undefined && !validTimeCoverage(value.time_coverage))
1148
+ return undefined;
1133
1149
  const ref = {
1134
1150
  dataset_id: value.dataset_id,
1135
1151
  task_id: value.task_id,
@@ -1143,6 +1159,7 @@ function validateStoredDatasetManifest(value, datasetId) {
1143
1159
  captured_at: value.captured_at,
1144
1160
  retention_until: value.retention_until,
1145
1161
  params_digest: value.params_digest,
1162
+ ...(value.time_coverage === undefined ? {} : { time_coverage: value.time_coverage }),
1146
1163
  };
1147
1164
  return {
1148
1165
  ...ref,
@@ -15,6 +15,21 @@ export interface AxisBounds {
15
15
  data_from: string | null;
16
16
  data_to: string | null;
17
17
  }
18
+ /**
19
+ * Host-computed coverage for a DatasetRef. `format: 'none'` is intentional: a quote source
20
+ * without a time column must not look like it has an as-of time.
21
+ */
22
+ export interface DatasetTimeCoverage {
23
+ column: string | null;
24
+ from: string | null;
25
+ to: string | null;
26
+ format: 'epoch_ms' | 'date_string' | 'datetime_string' | 'none';
27
+ }
28
+ /**
29
+ * Pick one time column by name and return its real min/max. This is a host fact for DatasetRef;
30
+ * it deliberately does not convert quote times across markets (their offsets stay in the string).
31
+ */
32
+ export declare function summarizeTimeCoverage(rows: unknown[]): DatasetTimeCoverage;
18
33
  /**
19
34
  * Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
20
35
  * to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
@@ -126,6 +126,79 @@ export function probeTimeAxis(column, values) {
126
126
  return { column, value_format: 'date_string', aligned_to: 'calendar_day' };
127
127
  return { column, value_format: 'unknown' };
128
128
  }
129
+ const TIME_COLUMN_PRIORITY = [
130
+ 'quote_time', 'event_time_beijing', 'datetime', 'report_date', 'trade_date',
131
+ 'stat_date', 'quote_date', 'collect_date', 'date', 'time',
132
+ 'quote_time_ms', 'event_time_ms', 'report_date_ms', 'trade_date_ms',
133
+ 'stat_date_ms', 'quote_date_ms', 'collect_date_ms', 'date_ms', 'time_ms',
134
+ ];
135
+ const COVERAGE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
136
+ const COVERAGE_DATETIME_RE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})?$/;
137
+ function coverageOfColumn(column, rows) {
138
+ const values = [];
139
+ for (const row of rows) {
140
+ const cell = cellOf(row, column);
141
+ if (!cell.present || cell.value === null || cell.value === undefined)
142
+ continue;
143
+ values.push(cell.value);
144
+ }
145
+ if (values.length === 0)
146
+ return undefined;
147
+ const numbers = values.filter((value) => typeof value === 'number' && Number.isFinite(value));
148
+ if (numbers.length === values.length) {
149
+ let min = Number.POSITIVE_INFINITY;
150
+ let max = Number.NEGATIVE_INFINITY;
151
+ for (const value of numbers) {
152
+ if (!Number.isSafeInteger(value) || value < MIN_EPOCH_MS || value > 8_640_000_000_000_000)
153
+ return undefined;
154
+ if (value < min)
155
+ min = value;
156
+ if (value > max)
157
+ max = value;
158
+ }
159
+ const axis = probeTimeAxis(column, numbers);
160
+ const zone = axis.time_zone ?? 'UTC';
161
+ const text = (value) => axis.aligned_to === 'local_midnight'
162
+ ? isoDateFromEpoch(value, zone)
163
+ : new Date(value).toISOString();
164
+ return { column, from: text(min), to: text(max), format: 'epoch_ms' };
165
+ }
166
+ const strings = values.filter((value) => typeof value === 'string');
167
+ if (strings.length !== values.length)
168
+ return undefined;
169
+ const trimmed = strings.map((value) => value.trim());
170
+ if (trimmed.every((value) => COVERAGE_DATE_RE.test(value))) {
171
+ const sorted = [...trimmed].sort();
172
+ return { column, from: sorted[0], to: sorted[sorted.length - 1], format: 'date_string' };
173
+ }
174
+ if (trimmed.every((value) => COVERAGE_DATETIME_RE.test(value))) {
175
+ const sorted = [...trimmed].map((value) => value.replace('T', ' ')).sort();
176
+ return { column, from: sorted[0], to: sorted[sorted.length - 1], format: 'datetime_string' };
177
+ }
178
+ return undefined;
179
+ }
180
+ /**
181
+ * Pick one time column by name and return its real min/max. This is a host fact for DatasetRef;
182
+ * it deliberately does not convert quote times across markets (their offsets stay in the string).
183
+ */
184
+ export function summarizeTimeCoverage(rows) {
185
+ if (rows.length === 0)
186
+ return { column: null, from: null, to: null, format: 'none' };
187
+ const first = rows.find((row) => isRecordLike(row));
188
+ if (!isRecordLike(first))
189
+ return { column: null, from: null, to: null, format: 'none' };
190
+ const keys = Object.keys(first);
191
+ const ordered = [
192
+ ...TIME_COLUMN_PRIORITY.filter((column) => keys.includes(column)),
193
+ ...keys.filter((column) => !TIME_COLUMN_PRIORITY.includes(column) && /(^|_)(date|time)(_|$)|_ms$/i.test(column)),
194
+ ];
195
+ for (const column of ordered) {
196
+ const coverage = coverageOfColumn(column, rows);
197
+ if (coverage !== undefined)
198
+ return coverage;
199
+ }
200
+ return { column: null, from: null, to: null, format: 'none' };
201
+ }
129
202
  /**
130
203
  * Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
131
204
  * to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
@@ -29,4 +29,4 @@ export interface DataCollectorDiagnostics {
29
29
  }
30
30
  export declare function registerDataCollectorTools(ctx: Context, hub: DataCollectorHub, diagnostics?: DataCollectorDiagnostics): void;
31
31
  /** Model-visible DatasetRef field whitelist (tests and host assertions). */
32
- export declare const DATASET_REF_FIELDS: readonly ["dataset_id", "task_id", "session_id", "artifact_ref", "format", "capability", "source_label", "schema", "row_count", "captured_at", "retention_until", "params_digest"];
32
+ export declare const DATASET_REF_FIELDS: readonly ["dataset_id", "task_id", "session_id", "artifact_ref", "format", "capability", "source_label", "schema", "row_count", "captured_at", "retention_until", "params_digest", "time_coverage"];