@v587d/capital-generation 2.15.1 → 2.16.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 +62 -0
- package/README.md +7 -7
- package/automation/client.js +47 -17
- package/automation/client.src.cjs +294 -83
- package/automation/contract.js +4 -0
- package/automation/defaults.js +2 -0
- package/automation/index.js +11 -7
- package/automation/package.json +2 -1
- package/automation/route.js +8 -1
- package/automation/runner.js +28 -6
- package/automation/service.js +89 -0
- package/automation/settings.js +6 -0
- package/automation/workspaces.js +113 -13
- package/chart-ui/package.json +1 -1
- package/config/client.js +12 -5
- package/config/client.src.cjs +223 -0
- package/config/index.js +2 -0
- package/config/package.json +1 -1
- package/lib/agents/root-tool-policy.d.ts +3 -3
- package/lib/agents/root-tool-policy.js +4 -2
- package/lib/data-collector/dataset-tools.js +22 -0
- package/lib/data-collector/describe.js +1 -0
- package/lib/data-collector/hub.js +5 -0
- package/lib/data-collector/store.d.ts +5 -0
- package/lib/data-collector/store.js +17 -0
- package/lib/data-collector/time-axis.d.ts +15 -0
- package/lib/data-collector/time-axis.js +73 -0
- package/lib/data-collector/tools.d.ts +1 -1
- package/lib/data-collector/tools.js +12 -1
- package/lib/index.d.ts +5 -0
- package/lib/index.js +2 -0
- package/lib/sources/tencent-http.js +7 -2
- package/lib/sources/tencent-public-core.d.ts +2 -0
- package/lib/sources/tencent-public-core.js +5 -0
- package/lib/sources/wind-mcp.js +3 -3
- package/lib/sources-entry.d.ts +1 -1
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/sources.d.ts +58 -0
- package/lib/web-retriever/sources.js +290 -0
- package/lib/web-retriever/tools.js +199 -8
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +1 -1
- package/preset/capital-generation/agent.patch.yml +20 -19
- package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
- package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
- package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
- package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
- package/watchlist/package.json +1 -1
package/config/client.src.cjs
CHANGED
|
@@ -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 地址'),
|
package/config/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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',
|
|
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"];
|