@v587d/capital-generation 2.15.0 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +7 -12
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/automation/TABLER-ICONS-LICENSE +21 -0
  5. package/automation/builtins.js +237 -10
  6. package/automation/client.js +79 -17
  7. package/automation/client.src.cjs +873 -210
  8. package/automation/contract.js +4 -0
  9. package/automation/defaults.js +6 -4
  10. package/automation/index.js +12 -7
  11. package/automation/package.json +5 -2
  12. package/automation/route.js +9 -2
  13. package/automation/runner.js +33 -6
  14. package/automation/scheduler.js +34 -29
  15. package/automation/schema.js +39 -5
  16. package/automation/service.js +102 -10
  17. package/automation/settings.js +6 -0
  18. package/automation/store.js +64 -21
  19. package/automation/task-icons.js +75 -0
  20. package/automation/workspaces.js +113 -13
  21. package/chart-ui/client.js +2 -2
  22. package/chart-ui/package.json +1 -1
  23. package/config/client.js +19 -5
  24. package/config/client.src.cjs +365 -44
  25. package/config/index.js +7 -5
  26. package/config/package.json +1 -1
  27. package/lib/agents/root-tool-policy.d.ts +3 -3
  28. package/lib/agents/root-tool-policy.js +4 -2
  29. package/lib/chart/runtime.d.ts +1 -1
  30. package/lib/chart/runtime.js +95 -0
  31. package/lib/chart/series.d.ts +4 -4
  32. package/lib/chart/series.js +44 -10
  33. package/lib/data-collector/dataset-tools.js +22 -0
  34. package/lib/data-collector/describe.js +1 -0
  35. package/lib/data-collector/hub.js +5 -0
  36. package/lib/data-collector/store.d.ts +5 -0
  37. package/lib/data-collector/store.js +17 -0
  38. package/lib/data-collector/time-axis.d.ts +15 -0
  39. package/lib/data-collector/time-axis.js +73 -0
  40. package/lib/data-collector/tools.d.ts +1 -1
  41. package/lib/data-collector/tools.js +12 -1
  42. package/lib/index.d.ts +5 -0
  43. package/lib/index.js +7 -5
  44. package/lib/sources/tencent-http.js +7 -2
  45. package/lib/sources/tencent-public-core.d.ts +2 -0
  46. package/lib/sources/tencent-public-core.js +5 -0
  47. package/lib/sources/wind-mcp.js +3 -3
  48. package/lib/sources-entry.d.ts +1 -1
  49. package/lib/web-retriever/local-fetch.d.ts +1 -1
  50. package/lib/web-retriever/local-fetch.js +1 -1
  51. package/lib/web-retriever/sources.d.ts +58 -0
  52. package/lib/web-retriever/sources.js +290 -0
  53. package/lib/web-retriever/tools.js +199 -8
  54. package/lib/web-retriever/wind-client.d.ts +1 -1
  55. package/lib/web-retriever/wind-client.js +1 -1
  56. package/package.json +1 -1
  57. package/preset/capital-generation/agent.patch.yml +20 -19
  58. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
  59. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
  60. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
  61. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
  62. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
  63. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
  64. 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,14 +67,24 @@ 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
- runTimeoutMinutes: 30,
74
+ runTimeoutMinutes: 60,
70
75
  idleTimeoutMinutes: 5,
71
76
  maxSteps: 300,
72
77
  maxToolCalls: 1_200,
73
78
  inAppNotifications: true,
74
79
  }
80
+ /** Mirrors `automation/defaults.js`; out-of-range values get an inline error instead of a silent clamp. */
81
+ const AUTOMATION_RANGES = {
82
+ tickMs: { min: 30_000, max: 3_600_000, unit: 'automationUnitMs' },
83
+ runTimeoutMinutes: { min: 5, max: 120, unit: 'automationUnitMinutes' },
84
+ idleTimeoutMinutes: { min: 1, max: 30, unit: 'automationUnitMinutes' },
85
+ maxSteps: { min: 100, max: 2_000, unit: 'automationUnitSteps' },
86
+ maxToolCalls: { min: 100, max: 5_000, unit: 'automationUnitCalls' },
87
+ }
75
88
  const SELECTED_SKILL_FIELDS = SELECTED_SKILL_CATALOG.map(({ key }) => key)
76
89
  // Stable orders: facet counts change with the other dimension, but chips must not jump under the cursor.
77
90
  const SELECTED_SKILL_TRAITS = selectedSkillTraitOrder()
@@ -102,13 +115,27 @@ const zh = {
102
115
  localFetchLabel: '允许启动本地提取网页内容',
103
116
  localFetchHint: '开启后本地抓取兜底(回执来源 local-http)。保存后新自动模式会话生效。',
104
117
  automationTitle: '投研自动化',
105
- automationHint: '调度器与无人值守运行的默认上限;任务详情里的设置优先。改动在下一个 tick 生效。',
106
- automationTick: '调度间隔(毫秒)',
107
- automationRunTimeout: 'wall-clock 上限(分钟)',
118
+ automationScheduleGroup: '调度',
119
+ automationRunGroup: '单次运行上限',
120
+ automationNotifyGroup: '通知',
121
+ automationInAppHint: '运行结束、失败或超时后,在应用内通知里留一条记录。',
122
+ automationTick: '自动检查间隔(毫秒)',
123
+ automationRunTimeout: '最长运行时间(分钟)',
124
+ automationInvalid: '请填写 {min}–{max} {unit}',
125
+ automationUnitMs: '毫秒',
126
+ automationUnitMinutes: '分钟',
127
+ automationUnitSteps: '步',
128
+ automationUnitCalls: '次',
108
129
  automationIdleTimeout: '空闲超时(分钟)',
109
130
  automationMaxSteps: '最大步骤',
110
131
  automationMaxToolCalls: '最大工具调用',
111
132
  automationInApp: '写入应用内通知',
133
+ automationDefaultWorkspace: '默认工作区',
134
+ automationWorkspacePath: '默认位置:{path}',
135
+ automationWorkspaceMissing: '原工作区已被移除,运行会回退到专属默认工作区。',
136
+ automationWorkspaceLoading: '工作区列表读取中…',
137
+ automationWorkspaceUnavailable: '工作区列表暂时读不到;任务仍会使用默认工作区,切回本格会重试。',
138
+ automationWorkspaceFallback: '专属默认工作区',
112
139
  windLabel: 'Wind Alice API Key',
113
140
  windHint: 'Wind 金融信披类文档检索接口。保存后新自动模式会话生效。',
114
141
  paddleocrLabel: 'PaddleOCR 文档解析 Token',
@@ -136,7 +163,9 @@ const zh = {
136
163
  save: '保存',
137
164
  saving: '保存中…',
138
165
  readOnly: '本部署的设置为只读。',
139
- saveFailed: '本部署没有接受这些值,已保留供你修改。',
166
+ // A rejected or non-landing write (stale page revision, transport hiccup). Plain words only:
167
+ // the typed value is still there, so the next action is retry, not a restart.
168
+ saveFailed: '没有保存成功,请再试一次。',
140
169
  unavailable: '该插件当前未加载,暂时无法配置。',
141
170
  githubLabel: 'GitHub 仓库',
142
171
  npmLabel: 'npm 包页面',
@@ -157,13 +186,27 @@ const en = {
157
186
  localFetchLabel: 'Allow local web page extraction',
158
187
  localFetchHint: 'When on, a failed fetch falls back to local direct fetching (receipt via: local-http). Takes effect in new Auto mode sessions.',
159
188
  automationTitle: 'Research automation',
160
- automationHint: 'Scheduler and unattended-run defaults; per-task limits win. Changes apply on the next tick.',
161
- automationTick: 'Tick interval (ms)',
162
- automationRunTimeout: 'Wall-clock limit (minutes)',
189
+ automationScheduleGroup: 'Scheduling',
190
+ automationRunGroup: 'Per-run limits',
191
+ automationNotifyGroup: 'Notifications',
192
+ automationInAppHint: 'Leave an in-app notification when a run finishes, fails, or times out.',
193
+ automationTick: 'Auto-check interval (ms)',
194
+ automationRunTimeout: 'Max run time (minutes)',
195
+ automationInvalid: 'Enter a whole number from {min} to {max} {unit}',
196
+ automationUnitMs: 'ms',
197
+ automationUnitMinutes: 'minutes',
198
+ automationUnitSteps: 'steps',
199
+ automationUnitCalls: 'calls',
163
200
  automationIdleTimeout: 'Idle timeout (minutes)',
164
201
  automationMaxSteps: 'Max steps',
165
202
  automationMaxToolCalls: 'Max tool calls',
166
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',
167
210
  windLabel: 'Wind Alice API Key',
168
211
  windHint: 'Wind financial disclosure document retrieval interface. Takes effect in new Auto mode sessions.',
169
212
  paddleocrLabel: 'PaddleOCR document token',
@@ -191,7 +234,7 @@ const en = {
191
234
  save: 'Save',
192
235
  saving: 'Saving…',
193
236
  readOnly: 'This deployment stores settings read-only.',
194
- saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
237
+ saveFailed: "That didn't save. Please try again.",
195
238
  unavailable: 'This plugin is not loaded, so it cannot be configured right now.',
196
239
  githubLabel: 'GitHub repository',
197
240
  npmLabel: 'npm package page',
@@ -221,6 +264,7 @@ function installStyles() {
221
264
  .capital-config-panel[hidden] { display: none; }
222
265
  /* Host rows section: positive :has rules only, so any mismatch leaves it visible. */
223
266
  [data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="sources"]) section[data-plugin-rows] { display: none; }
267
+ [data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="automation"]) section[data-plugin-rows] { display: none; }
224
268
  [data-plugin-detail="${BUNDLE_NAME}"]:has(.capital-config-card[data-capital-config-tab="skills"]) section[data-plugin-rows] { display: none; }
225
269
  /* Internal roles must stay declared for delegation; hide their user-selectable cards in Settings. */
226
270
  [data-agent-preset-id="capital-data-junior"],
@@ -230,8 +274,21 @@ function installStyles() {
230
274
  .capital-config-secret-row label { flex: 0 1 auto; }
231
275
  .capital-config-secret-row label ~ span { order: 1; margin-left: auto; }
232
276
  .capital-config-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-top: 0.5px solid var(--dsw-alias-border-l2); }
277
+ .capital-config-automation { display: flex; flex-direction: column; gap: 16px; padding-top: 12px; }
278
+ .capital-config-automation-group { display: flex; flex-direction: column; gap: 8px; }
279
+ .capital-config-automation-group > .capital-config-group-label { color: var(--dsw-alias-label-secondary); font-size: 12px; font-weight: 500; line-height: 18px; }
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; }
282
+ .capital-config-number-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
283
+ .capital-config-number-head .capital-config-write-failed { font-size: 11px; line-height: 16px; }
233
284
  .capital-config-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px; }
234
- .capital-config-field 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; }
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); }
235
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); }
236
293
  .capital-config-switch-row { align-items: center; gap: 12px; display: flex; }
237
294
  .capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
@@ -290,6 +347,89 @@ function installStyles() {
290
347
  return () => style.remove()
291
348
  }
292
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
+
293
433
  class CapitalCardController {
294
434
  constructor(scope, ctx) {
295
435
  this.scope = scope
@@ -303,6 +443,12 @@ class CapitalCardController {
303
443
  this.selectedSkillsModeFailed = false
304
444
  this.automationWriting = new Set()
305
445
  this.automationFailed = new Set()
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
306
452
  // Row count is external: null = unknown, never 0; fetch only after the card is shown.
307
453
  this.bundleRows = null
308
454
  this.rowsRequested = false
@@ -357,7 +503,14 @@ class CapitalCardController {
357
503
  on: snapshot.value?.automation?.[field] !== false,
358
504
  writing: this.automationWriting.has(field),
359
505
  failed: this.automationFailed.has(field),
506
+ invalid: this.automationInvalid.has(field),
360
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
+ }),
361
514
  // `count === null` means unknown, not zero: the card decides whether to draw it.
362
515
  components: { count: this.bundleRows },
363
516
  }
@@ -471,18 +624,34 @@ class CapitalCardController {
471
624
  this.store.set(this.projection())
472
625
  }
473
626
 
627
+ /** Returns why the write did or did not land: the card restores the input only for `invalid`. */
474
628
  async writeAutomation(field, value) {
475
- if (!AUTOMATION_FIELDS.includes(field) || this.automationWriting.has(field)) return
629
+ if (!AUTOMATION_FIELDS.includes(field) || this.automationWriting.has(field)) return 'ignored'
476
630
  const snapshot = this.scope.getSnapshot()
477
- if (snapshot.status !== 'ready' || !snapshot.writable) return
478
- const next = field === 'inAppNotifications' ? value === true : Number(value)
479
- if (field !== 'inAppNotifications' && !Number.isFinite(next)) return
631
+ if (snapshot.status !== 'ready' || !snapshot.writable) return 'ignored'
632
+ let next
633
+ if (field === 'inAppNotifications') {
634
+ next = value === true
635
+ } else {
636
+ const { min, max } = AUTOMATION_RANGES[field]
637
+ const raw = typeof value === 'string' ? value.trim() : value
638
+ next = Number(raw)
639
+ if (raw === '' || raw === null || raw === undefined || !Number.isInteger(next) || next < min || next > max) {
640
+ // Point at the range and write nothing; the card puts the saved value back.
641
+ this.automationInvalid.add(field)
642
+ this.automationFailed.delete(field)
643
+ this.store.set(this.projection())
644
+ return 'invalid'
645
+ }
646
+ this.automationInvalid.delete(field)
647
+ }
480
648
  this.automationWriting.add(field)
481
649
  this.automationFailed.delete(field)
482
650
  this.store.set(this.projection())
483
651
  try {
484
- const current = this.scope.getSnapshot().value?.automation ?? { ...AUTOMATION_DEFAULTS }
485
- await this.scope.mutate([{ op: 'set', path: ['automation'], value: { ...AUTOMATION_DEFAULTS, ...current, [field]: next } }])
652
+ // Only leaves are volatile: writing the `automation` container is rejected by the settings
653
+ // service even though every child field is editable.
654
+ await this.scope.mutate([{ op: 'set', path: ['automation', field], value: next }])
486
655
  } catch {
487
656
  // The landed check below reports the failure.
488
657
  }
@@ -490,6 +659,12 @@ class CapitalCardController {
490
659
  const landed = this.scope.getSnapshot().value?.automation?.[field] ?? AUTOMATION_DEFAULTS[field]
491
660
  if (landed !== next) this.automationFailed.add(field)
492
661
  this.store.set(this.projection())
662
+ return landed === next ? 'saved' : 'failed'
663
+ }
664
+
665
+ clearAutomationInvalid(field) {
666
+ if (!this.automationInvalid.delete(field)) return
667
+ this.store.set(this.projection())
493
668
  }
494
669
 
495
670
  /**
@@ -525,6 +700,51 @@ class CapitalCardController {
525
700
  this.store.set(this.projection())
526
701
  }
527
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
+
528
748
  inject() {
529
749
  return {
530
750
  hooks: {
@@ -533,9 +753,12 @@ class CapitalCardController {
533
753
  ...this.model.actions(),
534
754
  toggleLocalFetch: (value) => this.toggleLocalFetch(value),
535
755
  writeAutomation: (field, value) => this.writeAutomation(field, value),
756
+ clearAutomationInvalid: (field) => this.clearAutomationInvalid(field),
536
757
  toggleSelectedSkill: (field, value) => this.toggleSelectedSkill(field, value),
537
758
  toggleSelectedSkillsMode: (auto) => this.toggleSelectedSkillsMode(auto),
538
759
  ensureBundleRows: () => this.ensureBundleRows(),
760
+ ensureWorkspaces: () => this.ensureWorkspaces(),
761
+ writeAutomationWorkspace: (value) => this.writeAutomationWorkspace(value),
539
762
  }
540
763
  }
541
764
  }
@@ -744,6 +967,10 @@ function CapitalCard(props) {
744
967
  const [presetFilter, setPresetFilter] = useState(SELECTED_SKILL_FILTER_ALL)
745
968
  // Fetch on mount: tab labels carry numbers; hooks must precede the unavailable return.
746
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])
747
974
  // Pinned order is snapshotted on tab entry so rows never move under the cursor.
748
975
  const enabledKeys = () => new Set(SELECTED_SKILL_CATALOG
749
976
  .filter((entry) => state.selectedSkills?.[entry.key]?.on === true)
@@ -789,37 +1016,128 @@ function CapitalCard(props) {
789
1016
  ),
790
1017
  state.localFetch?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
791
1018
  )
792
- const automationNumber = (field, label, min, max) => h('label', { className: 'capital-config-field' },
793
- h('span', { className: 'capital-config-switch-label' }, t(label)),
794
- h('input', {
795
- type: 'number',
796
- min,
797
- max,
798
- defaultValue: state.automation?.[field]?.value,
799
- disabled: disabled || state.saving,
800
- onBlur: (event) => props.writeAutomation(field, event.target.value),
801
- }),
802
- state.automation?.[field]?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
803
- )
804
- const automationSection = h('div', { className: 'capital-config-field' },
805
- h('span', { className: 'capital-config-switch-label' }, t('automationTitle')),
806
- h('p', { className: 'capital-config-hint' }, t('automationHint')),
807
- h('div', { className: 'capital-config-grid' },
808
- automationNumber('tickMs', 'automationTick', 30000, 3600000),
809
- automationNumber('runTimeoutMinutes', 'automationRunTimeout', 5, 120),
810
- automationNumber('idleTimeoutMinutes', 'automationIdleTimeout', 1, 30),
811
- automationNumber('maxSteps', 'automationMaxSteps', 10, 2000),
812
- automationNumber('maxToolCalls', 'automationMaxToolCalls', 10, 6000),
813
- ),
814
- h('label', { className: 'capital-config-switch-row' },
815
- h('span', { className: 'capital-config-switch-label' }, t('automationInApp')),
1019
+ const automationNumber = (field, label) => {
1020
+ const { min, max, unit } = AUTOMATION_RANGES[field]
1021
+ const stored = state.automation?.[field]?.value
1022
+ const invalid = state.automation?.[field]?.invalid === true
1023
+ const error = invalid
1024
+ ? t('automationInvalid', { min, max, unit: t(unit) })
1025
+ : state.automation?.[field]?.failed ? t('saveFailed') : null
1026
+ return h('label', { className: 'capital-config-number' },
1027
+ // Error sits on the title row (the card's usual spot), not under the box.
1028
+ h('span', { className: 'capital-config-number-head' },
1029
+ h('span', { className: 'capital-config-switch-label' }, t(label)),
1030
+ error ? h('span', {
1031
+ id: `capital-config-automation-${field}-error`,
1032
+ role: invalid ? 'alert' : 'status',
1033
+ className: 'capital-config-write-failed',
1034
+ }, error) : null,
1035
+ ),
816
1036
  h('input', {
817
- type: 'checkbox',
818
- checked: state.automation?.inAppNotifications?.on !== false,
1037
+ type: 'number',
1038
+ min,
1039
+ max,
1040
+ step: 1,
1041
+ defaultValue: stored,
819
1042
  disabled: disabled || state.saving,
820
- onChange: (event) => props.writeAutomation('inAppNotifications', event.target.checked),
1043
+ 'aria-invalid': invalid ? 'true' : undefined,
1044
+ 'aria-describedby': error ? `capital-config-automation-${field}-error` : undefined,
1045
+ onFocus: () => props.clearAutomationInvalid(field),
1046
+ onBlur: (event) => {
1047
+ const input = event.target
1048
+ return Promise.resolve(props.writeAutomation(field, input.value)).then((outcome) => {
1049
+ if (outcome === 'invalid' && stored !== undefined) input.value = String(stored)
1050
+ })
1051
+ },
821
1052
  }),
1053
+ )
1054
+ }
1055
+ const automationGroup = (labelKey, children) => h('div', { className: 'capital-config-automation-group' },
1056
+ h('span', { className: 'capital-config-group-label' }, t(labelKey)),
1057
+ ...children,
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,
822
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
+ )
1111
+ const automationSection = h('div', { className: 'capital-config-automation' },
1112
+ workspaceSection,
1113
+ automationGroup('automationScheduleGroup', [
1114
+ h('div', { className: 'capital-config-grid' },
1115
+ automationNumber('tickMs', 'automationTick'),
1116
+ ),
1117
+ ]),
1118
+ automationGroup('automationRunGroup', [
1119
+ h('div', { className: 'capital-config-grid' },
1120
+ automationNumber('runTimeoutMinutes', 'automationRunTimeout'),
1121
+ automationNumber('idleTimeoutMinutes', 'automationIdleTimeout'),
1122
+ automationNumber('maxSteps', 'automationMaxSteps'),
1123
+ automationNumber('maxToolCalls', 'automationMaxToolCalls'),
1124
+ ),
1125
+ ]),
1126
+ automationGroup('automationNotifyGroup', [
1127
+ h('div', { className: 'capital-config-switch-row' },
1128
+ h('div', { className: 'capital-config-switch-text' },
1129
+ h('span', { className: 'capital-config-switch-label' }, t('automationInApp')),
1130
+ h('p', { className: 'capital-config-hint' }, t('automationInAppHint')),
1131
+ ),
1132
+ h(Switch, {
1133
+ label: t('automationInApp'),
1134
+ checked: state.automation?.inAppNotifications?.on !== false,
1135
+ disabled: disabled || state.saving || state.automation?.inAppNotifications?.writing === true,
1136
+ onChange: (value) => props.writeAutomation('inAppNotifications', value),
1137
+ }),
1138
+ ),
1139
+ state.automation?.inAppNotifications?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
1140
+ ]),
823
1141
  )
824
1142
  const modeSection = h('div', { className: 'capital-config-selected-mode' },
825
1143
  h('div', { className: 'capital-config-switch-row' },
@@ -889,6 +1207,7 @@ function CapitalCard(props) {
889
1207
  const options = [
890
1208
  // A dirty draft while Save is off-screen must be announced on the tab.
891
1209
  { value: 'sources', label: state.dirty ? t('titleUnsaved') : t('title') },
1210
+ { value: 'automation', label: t('automationTitle') },
892
1211
  { value: 'skills', label: t('selectedSkillsTitle') },
893
1212
  { value: 'components', label: withCount(t('componentsTitle'), state.components?.count) },
894
1213
  ]
@@ -911,11 +1230,13 @@ function CapitalCard(props) {
911
1230
  setTab(next)
912
1231
  // Re-read on every switch to this tab: a previous failure retries here.
913
1232
  if (next === 'components') props.ensureBundleRows()
1233
+ if (next === 'automation') props.ensureWorkspaces()
914
1234
  // Re-pin only when entering Skills, so the row under the cursor never jumps.
915
1235
  if (next === 'skills') setPinnedSkills(enabledKeys())
916
1236
  },
917
1237
  }),
918
- panel('sources', [form, localFetchSection, automationSection]),
1238
+ panel('sources', [form, localFetchSection]),
1239
+ panel('automation', [automationSection]),
919
1240
  panel('skills', autoMode
920
1241
  ? [modeSection]
921
1242
  : [modeSection, h('div', { className: 'capital-config-skills-browser' },
package/config/index.js CHANGED
@@ -77,19 +77,21 @@ export const Config = z.object({
77
77
  fuyaoCredentialRef: z.string().default('FUYAO_API_KEY').volatile().description('Fuyao credentials 引用名'),
78
78
  ftshareCredentialRef: z.string().default('FTSHARE_API_KEY').volatile().description('FTShare(非凸科技)credentials 引用名'),
79
79
  automation: z.object({
80
- tickMs: z.number().default(60_000).volatile().description('投研自动化调度间隔毫秒(30000–3600000)'),
81
- runTimeoutMinutes: z.number().default(30).volatile().description('单次运行 wall-clock 上限分钟(5–120)'),
80
+ tickMs: z.number().default(60_000).volatile().description('投研自动化自动检查间隔(毫秒,30000–3600000)'),
81
+ runTimeoutMinutes: z.number().default(60).volatile().description('单次运行最长运行时间(分钟,5–120)'),
82
82
  idleTimeoutMinutes: z.number().default(5).volatile().description('单次运行空闲超时分钟(1–30)'),
83
- maxSteps: z.number().default(300).volatile().description('单次运行最大步骤数(10–2000)'),
84
- maxToolCalls: z.number().default(1_200).volatile().description('单次运行最大工具调用数(10–6000)'),
83
+ maxSteps: z.number().default(300).volatile().description('单次运行最大步骤数(100–2000)'),
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
- runTimeoutMinutes: 30,
89
+ runTimeoutMinutes: 60,
89
90
  idleTimeoutMinutes: 5,
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.0",
3
+ "version": "2.16.0",
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[];