@v587d/capital-generation 2.10.0 → 2.10.2

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 (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +20 -10
  3. package/chart-ui/package.json +1 -1
  4. package/config/client.js +36 -29
  5. package/config/client.src.cjs +228 -142
  6. package/config/index.js +8 -2
  7. package/config/package.json +1 -1
  8. package/lib/agents/selected-skills-provider.d.ts +29 -10
  9. package/lib/agents/selected-skills-provider.js +262 -139
  10. package/lib/agents/skill-usage.d.ts +22 -0
  11. package/lib/agents/skill-usage.js +92 -0
  12. package/lib/capital-settings.d.ts +12 -1
  13. package/lib/capital-settings.js +23 -3
  14. package/lib/index.d.ts +6 -3
  15. package/lib/index.js +10 -19
  16. package/lib/root-extras-entry.d.ts +2 -5
  17. package/lib/root-extras-entry.js +1 -3
  18. package/lib/selected-skills-entry.d.ts +14 -0
  19. package/lib/selected-skills-entry.js +43 -0
  20. package/lib/web-retriever/local-fetch.d.ts +1 -1
  21. package/lib/web-retriever/local-fetch.js +1 -1
  22. package/lib/web-retriever/wind-client.d.ts +1 -1
  23. package/lib/web-retriever/wind-client.js +1 -1
  24. package/package.json +11 -2
  25. package/preset/capital-generation/agent.patch.yml +34 -16
  26. package/preset/capital-generation/skills/capital-data-query/SKILL.md +19 -0
  27. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +4 -0
  28. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +3 -0
  29. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +7 -9
  30. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +3 -0
  31. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -0
  32. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +3 -0
  33. package/selected-skills/README.md +8 -6
  34. package/selected-skills/ROUTING.md +88 -0
  35. package/selected-skills/catalog.js +32 -121
  36. package/selected-skills/filter.js +70 -0
  37. package/selected-skills/routing.js +1059 -0
  38. package/watchlist/package.json +1 -1
@@ -5,13 +5,26 @@ const {
5
5
  SettingsSecretField,
6
6
  SettingsFormModel,
7
7
  SegmentedControl,
8
+ Pill,
8
9
  Switch,
10
+ Tag,
9
11
  Modal,
10
- writeClipboard,
11
12
  IconRightUpOutlineRegular,
12
- IconWarningTriangleOutlineRegular,
13
13
  } = require('@deepseek-ai/dsh-client-ui-primitives')
14
- const { SELECTED_SKILL_CATALOG, SELECTED_SKILL_TAG_LABELS, SELECTED_SKILLS_MAX_ENABLED } = require('../selected-skills/catalog.js')
14
+ const { SELECTED_SKILL_CATALOG, SELECTED_SKILL_TAG_LABELS } = require('../selected-skills/catalog.js')
15
+ const {
16
+ SELECTED_SKILL_FILTER_ALL,
17
+ filterSelectedSkills,
18
+ selectedSkillFacetCounts,
19
+ selectedSkillPresetTotals,
20
+ selectedSkillRoles,
21
+ selectedSkillTraitOrder,
22
+ } = require('../selected-skills/filter.js')
23
+ const {
24
+ SELECTED_SKILL_PRESETS,
25
+ SELECTED_SKILL_PRESET_ORDER,
26
+ SELECTED_SKILL_ROUTING,
27
+ } = require('../selected-skills/routing.js')
15
28
 
16
29
  /**
17
30
  * Two different strings: `ENTRY_ID` is the settings namespace (= profile entry
@@ -51,6 +64,9 @@ const DOC_URLS = {
51
64
  /** Render order = array order; one list drives status, refs, drafts and event refresh. */
52
65
  const CREDENTIAL_FIELDS = ['fuyao', 'ftshare', 'anysearch', 'wind', 'paddleocr']
53
66
  const SELECTED_SKILL_FIELDS = SELECTED_SKILL_CATALOG.map(({ key }) => key)
67
+ // Stable orders: facet counts change with the other dimension, but chips must not jump under the cursor.
68
+ const SELECTED_SKILL_TRAITS = selectedSkillTraitOrder()
69
+ const SELECTED_SKILL_PRESET_TOTALS = selectedSkillPresetTotals()
54
70
  const DEFAULT_REFS = {
55
71
  fuyao: FUYAO_DEFAULT_REF,
56
72
  ftshare: FTSHARE_DEFAULT_REF,
@@ -81,14 +97,17 @@ const zh = {
81
97
  paddleocrLabel: 'PaddleOCR 文档解析 Token',
82
98
  paddleocrHint: '配置 Token 后自动解析 PDF、图片。保存后新自动模式会话生效。',
83
99
  selectedSkillsTitle: '精选 Skills',
84
- selectedSkillsFilterLabel: '按能力筛选',
85
- selectedSkillsEmpty: '没有匹配这个标签的 Skill。',
86
- // Cap blocks adding only; existing selections keep working (runtime does not truncate).
87
- // Reason lives on the warning icon's title / aria-label; {count} is appended in code.
88
- selectedSkillsCapTooltip: 'Skill已达上限 ({count})。',
89
- selectedSkillsCopyName: '复制技能名',
90
- selectedSkillsCopied: '已复制',
91
- selectedSkillsDetail: '详情',
100
+ selectedSkillsModeLabel: '智能装载 Skills',
101
+ selectedSkillsModeHint: '开启后,由大模型根据用户提问,智能装载合适的精选「Skills」。',
102
+ selectedSkillsFilterLabel: '筛选精选 Skills',
103
+ selectedSkillsTraitGroup: 'Skill 特点',
104
+ selectedSkillsPresetGroup: '适用预设',
105
+ selectedSkillsAll: '全部',
106
+ selectedSkillsEmpty: '没有符合当前筛选条件的 Skill。',
107
+ selectedSkillsWhen: '何时使用',
108
+ selectedSkillsKeywords: '关键词',
109
+ selectedSkillsOpenDetail: '查看 Skill 原文',
110
+ selectedSkillsPresetUnavailable: '该预设不加载精选 Skills',
92
111
  selectedSkillsDetailClose: '关闭',
93
112
  selectedSkillsDocLoading: '原文读取中…',
94
113
  selectedSkillsDocFailed: '原文读不到:这份快照没随包落地,或暂时读不出来。',
@@ -125,12 +144,17 @@ const en = {
125
144
  paddleocrLabel: 'PaddleOCR document token',
126
145
  paddleocrHint: 'Configure the token to parse PDFs and images automatically. Takes effect in new Auto mode sessions.',
127
146
  selectedSkillsTitle: 'Selected Skills',
128
- selectedSkillsFilterLabel: 'Filter by capability',
129
- selectedSkillsEmpty: 'No skills match this tag.',
130
- selectedSkillsCapTooltip: 'Skill limit reached ({count}).',
131
- selectedSkillsCopyName: 'Copy skill name',
132
- selectedSkillsCopied: 'Copied',
133
- selectedSkillsDetail: 'Details',
147
+ selectedSkillsModeLabel: 'Auto-load Skills',
148
+ selectedSkillsModeHint: 'When on, the model picks the relevant selected Skills for each question.',
149
+ selectedSkillsFilterLabel: 'Filter selected Skills',
150
+ selectedSkillsTraitGroup: 'Skill traits',
151
+ selectedSkillsPresetGroup: 'Preset',
152
+ selectedSkillsAll: 'All',
153
+ selectedSkillsEmpty: 'No skills match the current filters.',
154
+ selectedSkillsWhen: 'Use when',
155
+ selectedSkillsKeywords: 'Keywords',
156
+ selectedSkillsOpenDetail: 'Open skill source',
157
+ selectedSkillsPresetUnavailable: 'This preset does not load selected Skills.',
134
158
  selectedSkillsDetailClose: 'Close',
135
159
  selectedSkillsDocLoading: 'Loading the source…',
136
160
  selectedSkillsDocFailed: 'The source could not be read: this snapshot is not in the installed package, or it cannot be opened right now.',
@@ -166,7 +190,6 @@ function installStyles() {
166
190
  style.dataset.capitalConfig = 'true'
167
191
  style.textContent = `
168
192
  .capital-config-card { display: flex; flex-direction: column; gap: 12px; }
169
- .capital-config-title { margin: 0; font-size: 14px; font-weight: 500; line-height: 20px; }
170
193
  /* Declare display only when not hidden, or the hidden attribute loses to author display. */
171
194
  .capital-config-panel { flex-direction: column; gap: 12px; }
172
195
  .capital-config-panel:not([hidden]) { display: flex; }
@@ -182,25 +205,39 @@ function installStyles() {
182
205
  .capital-config-secret-row label { flex: 0 1 auto; }
183
206
  .capital-config-secret-row label ~ span { order: 1; margin-left: auto; }
184
207
  .capital-config-field { display: flex; flex-direction: column; gap: 6px; padding: 12px 0; border-top: 0.5px solid var(--dsw-alias-border-l2); }
185
- /* Filter column stays sticky while the 35-row list scrolls; padding-top matches the 24px rhythm. */
186
- .capital-config-selected-layout { display: flex; align-items: flex-start; gap: 16px; padding-top: 12px; }
187
- .capital-config-selected-filter { position: sticky; top: 12px; flex: 0 0 132px; display: flex; flex-direction: column; align-items: stretch; gap: 3px; }
188
- .capital-config-selected-filter-button { display: flex; align-items: baseline; justify-content: space-between; gap: 6px; min-height: 26px; padding: 3px 8px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12px; line-height: 18px; text-align: left; cursor: pointer; }
189
- .capital-config-selected-filter-button:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
190
- .capital-config-selected-filter-button[aria-pressed="true"] { border-color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); color: var(--dsw-alias-label-primary); }
191
- .capital-config-selected-filter-count { flex: none; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
192
- .capital-config-selected-filter-button[aria-pressed="true"] .capital-config-selected-filter-count { color: inherit; }
193
- .capital-config-selected-list { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
194
- .capital-config-skill-tags { display: flex; flex-wrap: wrap; gap: 4px; }
195
- .capital-config-skill-tag { padding: 1px 5px; border-radius: 4px; background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
196
- .capital-config-skill-heading { min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
197
- .capital-config-skill-actions { flex: none; display: inline-flex; align-items: center; gap: 6px; }
198
- .capital-config-skill-action { flex: none; min-height: 22px; padding: 2px 8px; border: 0.5px solid var(--dsw-alias-border-l2); border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent); color: var(--dsw-alias-brand-primary); font: inherit; font-size: 11px; line-height: 16px; white-space: nowrap; cursor: pointer; }
199
- .capital-config-skill-action:hover { border-color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent); }
200
- .capital-config-skill-action:focus-visible { outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 1px; }
201
- .capital-config-skill-repo { display: inline-flex; align-items: center; gap: 3px; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; text-decoration: none; }
202
- .capital-config-skill-repo:hover { color: var(--dsw-alias-brand-primary); text-decoration: underline; }
208
+ .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); }
209
+ .capital-config-switch-row { align-items: center; gap: 12px; display: flex; }
210
+ .capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
211
+ .capital-config-switch-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }
212
+ .capital-config-hint { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
213
+ .capital-config-skills-browser { display: flex; flex-direction: column; gap: 16px; }
214
+ .capital-config-filters { display: flex; flex-direction: column; gap: 10px; padding: 12px 14px; border: 0.5px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); }
215
+ .capital-config-filter-row { display: flex; align-items: flex-start; gap: 12px; }
216
+ .capital-config-filter-label { flex: 0 0 auto; min-width: 64px; padding-top: 3px; color: var(--dsw-alias-label-tertiary); font-size: 11.5px; font-weight: 500; line-height: 18px; }
217
+ .capital-config-filter-options { display: flex; flex: 1 1 auto; flex-wrap: wrap; gap: 4px; min-width: 0; }
218
+ .capital-config-filter-pill { height: 22px; padding: 0 7px; gap: 5px; font-size: 11.5px; line-height: 16px; }
219
+ .capital-config-filter-pill:disabled { opacity: 0.45; cursor: not-allowed; }
220
+ .capital-config-filter-pill:disabled:hover { background: var(--dsw-alias-bg-layer-2); }
221
+ .capital-config-filter-count { color: var(--dsw-alias-label-tertiary); font-size: 10px; line-height: 14px; font-variant-numeric: tabular-nums; }
222
+ .capital-config-filter-pill[aria-pressed="true"] .capital-config-filter-count { color: inherit; opacity: 0.72; }
223
+ .capital-config-selected-list { display: flex; flex-direction: column; border-top: 0.5px solid var(--dsw-alias-border-l1); }
224
+ .capital-config-skill-item { border-bottom: 0.5px solid var(--dsw-alias-border-l1); }
225
+ .capital-config-skill-row { display: flex; align-items: flex-start; gap: 16px; padding: 14px 2px; }
226
+ .capital-config-skill-main { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; gap: 6px; }
227
+ .capital-config-skill-heading { min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
228
+ .capital-config-skill-name { padding: 0; border: 0; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; font-weight: 500; line-height: 20px; text-align: left; cursor: pointer; }
229
+ .capital-config-skill-name:hover { color: var(--dsw-alias-brand-primary); }
230
+ .capital-config-skill-name:focus-visible { outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 2px; }
231
+ .capital-config-skill-tags, .capital-config-skill-presets { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 4px; }
232
+ .capital-config-skill-tag { font-weight: 400; }
233
+ .capital-config-skill-preset { font-weight: 500; }
203
234
  .capital-config-skill-description { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
235
+ .capital-config-skill-meta { display: grid; grid-template-columns: max-content minmax(0, 1fr); column-gap: 10px; row-gap: 2px; margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
236
+ .capital-config-skill-meta dt { color: var(--dsw-alias-label-tertiary); font-weight: 500; white-space: nowrap; }
237
+ .capital-config-skill-meta dd { min-width: 0; margin: 0; }
238
+ .capital-config-skill-repo { display: inline-flex; align-items: center; gap: 3px; width: max-content; max-width: 100%; margin-top: 2px; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; text-decoration: none; }
239
+ .capital-config-skill-repo:hover { color: var(--dsw-alias-brand-primary); text-decoration: underline; }
240
+ .capital-config-selected-empty { margin: 0; padding: 28px 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; text-align: center; }
204
241
  /* Official Modal defaults to 380px; the div prefix outranks the hashed .dialog class. */
205
242
  div.capital-config-skill-modal { width: min(820px, 100%); max-height: 100%; }
206
243
  /* Scroll the body so the dialog's overflow:hidden cannot clip it. */
@@ -215,12 +252,6 @@ function installStyles() {
215
252
  .capital-config-doc-link { display: inline-flex; align-items: center; gap: 3px; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 20px; white-space: nowrap; text-decoration: none; }
216
253
  .capital-config-doc-link:hover { color: var(--dsw-alias-brand-primary); text-decoration: underline; }
217
254
  .capital-config-doc-link:focus-visible { border-radius: 3px; outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 2px; }
218
- .capital-config-switch-row { align-items: center; gap: 12px; display: flex; }
219
- .capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
220
- .capital-config-switch-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }
221
- .capital-config-hint { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
222
- /* --dsw-alias-label-error is undefined in the theme, so state tokens only. */
223
- .capital-config-skill-cap-icon { display: inline-flex; align-items: center; flex: none; padding: 2px; margin: -2px; color: var(--dsw-alias-state-warn-primary); cursor: help; }
224
255
  .capital-config-write-failed { margin: 0; color: var(--dsw-alias-state-error-primary); font-size: 12px; line-height: 18px; }
225
256
  /* Own inline-flex group so the host's wrapping never splits the two links. */
226
257
  .capital-config-plugin-links { flex: none; align-items: center; gap: 2px; display: inline-flex; }
@@ -241,6 +272,8 @@ class CapitalCardController {
241
272
  this.localFetchFailed = false
242
273
  this.selectedSkillWriting = new Set()
243
274
  this.selectedSkillFailed = new Set()
275
+ this.selectedSkillsModeWriting = false
276
+ this.selectedSkillsModeFailed = false
244
277
  // Row count is external: null = unknown, never 0; fetch only after the card is shown.
245
278
  this.bundleRows = null
246
279
  this.rowsRequested = false
@@ -285,6 +318,11 @@ class CapitalCardController {
285
318
  writing: this.selectedSkillWriting.has(field),
286
319
  failed: this.selectedSkillFailed.has(field),
287
320
  }])),
321
+ selectedSkillsMode: {
322
+ auto: snapshot.value?.selectedSkillsMode === 'auto',
323
+ writing: this.selectedSkillsModeWriting,
324
+ failed: this.selectedSkillsModeFailed,
325
+ },
288
326
  // `count === null` means unknown, not zero: the card decides whether to draw it.
289
327
  components: { count: this.bundleRows },
290
328
  }
@@ -362,6 +400,24 @@ class CapitalCardController {
362
400
  this.store.set(this.projection())
363
401
  }
364
402
 
403
+ async toggleSelectedSkillsMode(auto) {
404
+ if (this.selectedSkillsModeWriting) return
405
+ const snapshot = this.scope.getSnapshot()
406
+ if (snapshot.status !== 'ready' || !snapshot.writable) return
407
+ this.selectedSkillsModeWriting = true
408
+ this.selectedSkillsModeFailed = false
409
+ this.store.set(this.projection())
410
+ try {
411
+ await this.scope.mutate([{ op: 'set', path: ['selectedSkillsMode'], value: auto ? 'auto' : 'manual' }])
412
+ } catch {
413
+ // The landed check below reports the failure.
414
+ }
415
+ this.selectedSkillsModeWriting = false
416
+ const landed = (this.scope.getSnapshot().value?.selectedSkillsMode === 'auto') === auto
417
+ if (!landed) this.selectedSkillsModeFailed = true
418
+ this.store.set(this.projection())
419
+ }
420
+
365
421
  async toggleSelectedSkill(field, value) {
366
422
  if (!SELECTED_SKILL_FIELDS.includes(field) || this.selectedSkillWriting.has(field)) return
367
423
  const snapshot = this.scope.getSnapshot()
@@ -421,6 +477,7 @@ class CapitalCardController {
421
477
  ...this.model.actions(),
422
478
  toggleLocalFetch: (value) => this.toggleLocalFetch(value),
423
479
  toggleSelectedSkill: (field, value) => this.toggleSelectedSkill(field, value),
480
+ toggleSelectedSkillsMode: (auto) => this.toggleSelectedSkillsMode(auto),
424
481
  ensureBundleRows: () => this.ensureBundleRows(),
425
482
  }
426
483
  }
@@ -503,27 +560,27 @@ function loadSkillDoc(name) {
503
560
  return pending
504
561
  }
505
562
 
506
- const COPIED_FLASH_MS = 1600
563
+ function presetLabel(role, locale) {
564
+ return SELECTED_SKILL_PRESETS[role]?.[locale] ?? role
565
+ }
566
+
567
+ /** Routing text stays one sentence; split the keyword tail so both lines share a type scale. */
568
+ function splitUseWhen(value, locale) {
569
+ const marker = locale === 'en' ? 'Triggers on ' : ';关键词:'
570
+ const index = value.lastIndexOf(marker)
571
+ if (index <= 0) return { when: value, keywords: '' }
572
+ return {
573
+ when: value.slice(0, index).trim(),
574
+ keywords: value.slice(index + marker.length).trim().replace(locale === 'en' ? /\.$/ : /。$/, ''),
575
+ }
576
+ }
507
577
 
508
- function SelectedSkillRow({ entry, state, disabled, capReached = false, capTooltip, onChange, t }) {
578
+ function SelectedSkillRow({ entry, state, disabled, onChange, t }) {
509
579
  const locale = skillLocale()
510
580
  const label = entry.label[locale]
511
581
  const repositoryUrl = `https://github.com/${entry.owner}/${entry.repository}`
512
- const copyTimer = useRef(null)
513
- const [copied, setCopied] = useState(false)
514
582
  const [detailOpen, setDetailOpen] = useState(false)
515
583
  const [doc, setDoc] = useState(null)
516
- useEffect(() => () => {
517
- if (copyTimer.current !== null) clearTimeout(copyTimer.current)
518
- }, [])
519
- const copyName = async () => {
520
- const accepted = await writeClipboard(entry.name)
521
- // Do not flip the label if the clipboard rejected it.
522
- if (!accepted) return
523
- setCopied(true)
524
- if (copyTimer.current !== null) clearTimeout(copyTimer.current)
525
- copyTimer.current = setTimeout(() => setCopied(false), COPIED_FLASH_MS)
526
- }
527
584
  const prefetchDoc = () => {
528
585
  if (doc?.status === 'ready' || doc?.status === 'loading') return
529
586
  if (skillDocs.has(entry.name)) {
@@ -544,45 +601,56 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
544
601
  : doc?.status === 'failed'
545
602
  ? h('p', { className: 'capital-config-skill-doc-state' }, t(doc.code === 'route_missing' ? 'selectedSkillsDocStale' : 'selectedSkillsDocFailed'))
546
603
  : h('p', { className: 'capital-config-skill-doc-state' }, t('selectedSkillsDocLoading'))
547
- return h('div', { className: 'capital-config-field' },
548
- h('div', { className: 'capital-config-switch-row' },
549
- h('div', { className: 'capital-config-switch-text' },
604
+ const { when, keywords } = splitUseWhen(SELECTED_SKILL_ROUTING[entry.name]?.useWhen?.[locale] ?? '', locale)
605
+ const roles = entry.roles ?? []
606
+ return h('div', { className: 'capital-config-skill-item' },
607
+ h('div', { className: 'capital-config-skill-row' },
608
+ h('div', { className: 'capital-config-skill-main' },
550
609
  h('div', { className: 'capital-config-skill-heading' },
551
- h('span', { className: 'capital-config-title' }, label),
552
- h('div', { className: 'capital-config-skill-actions' },
553
- // Copy name only on enabled rows; details has no gate.
554
- state?.on === true ? h('button', {
555
- type: 'button',
556
- className: 'capital-config-skill-action',
557
- title: `${t('selectedSkillsCopyName')}: ${entry.name}`,
558
- 'aria-label': `${t('selectedSkillsCopyName')}: ${entry.name}`,
559
- onClick: copyName,
560
- }, copied ? t('selectedSkillsCopied') : t('selectedSkillsCopyName')) : null,
561
- h('button', {
562
- type: 'button',
563
- className: 'capital-config-skill-action',
564
- 'aria-label': `${t('selectedSkillsDetail')}: ${entry.name}`,
565
- 'aria-haspopup': 'dialog',
566
- // Prefetch on hover / focus so the modal opens with content; click is the action.
567
- onPointerEnter: prefetchDoc,
568
- onFocus: prefetchDoc,
569
- onClick: () => {
570
- prefetchDoc()
571
- setDetailOpen(true)
572
- },
573
- }, t('selectedSkillsDetail')),
574
- h(Modal, {
575
- open: detailOpen,
576
- onClose: () => setDetailOpen(false),
577
- title: label,
578
- description: entry.name,
579
- closeLabel: t('selectedSkillsDetailClose'),
580
- className: 'capital-config-skill-modal',
581
- contentClassName: 'capital-config-skill-modal-content',
582
- }, detailContent),
583
- ),
610
+ h('button', {
611
+ type: 'button',
612
+ className: 'capital-config-skill-name',
613
+ title: t('selectedSkillsOpenDetail'),
614
+ 'aria-haspopup': 'dialog',
615
+ // Prefetch on hover / focus so the modal opens with content; click is the action.
616
+ onPointerEnter: prefetchDoc,
617
+ onFocus: prefetchDoc,
618
+ onClick: () => {
619
+ prefetchDoc()
620
+ setDetailOpen(true)
621
+ },
622
+ }, label),
623
+ h('div', { className: 'capital-config-skill-tags', role: 'group', 'aria-label': t('selectedSkillsTraitGroup') },
624
+ entry.tags.map((tag) => h(Tag, {
625
+ key: tag,
626
+ tone: 'neutral',
627
+ className: 'capital-config-skill-tag',
628
+ }, SELECTED_SKILL_TAG_LABELS[locale][tag] ?? tag))),
629
+ roles.length > 0 ? h('div', { className: 'capital-config-skill-presets', role: 'group', 'aria-label': t('selectedSkillsPresetGroup') },
630
+ roles.map((role) => h(Tag, {
631
+ key: role,
632
+ tone: 'info',
633
+ className: 'capital-config-skill-preset',
634
+ },
635
+ h('span', { className: 'capital-config-sr-only' }, `${t('selectedSkillsPresetGroup')}:`),
636
+ presetLabel(role, locale)))) : null,
637
+ h(Modal, {
638
+ open: detailOpen,
639
+ onClose: () => setDetailOpen(false),
640
+ title: label,
641
+ description: entry.name,
642
+ closeLabel: t('selectedSkillsDetailClose'),
643
+ className: 'capital-config-skill-modal',
644
+ contentClassName: 'capital-config-skill-modal-content',
645
+ }, detailContent),
584
646
  ),
585
647
  h('p', { className: 'capital-config-skill-description' }, entry.description[locale]),
648
+ h('dl', { className: 'capital-config-skill-meta' },
649
+ h('dt', { key: 'when-label' }, t('selectedSkillsWhen')),
650
+ h('dd', { key: 'when' }, when),
651
+ keywords ? h('dt', { key: 'keywords-label' }, t('selectedSkillsKeywords')) : null,
652
+ keywords ? h('dd', { key: 'keywords' }, keywords) : null,
653
+ ),
586
654
  h('a', {
587
655
  className: 'capital-config-skill-repo',
588
656
  href: repositoryUrl,
@@ -590,22 +658,11 @@ function SelectedSkillRow({ entry, state, disabled, capReached = false, capToolt
590
658
  rel: 'noopener noreferrer',
591
659
  title: t('selectedSkillsOpenRepo'),
592
660
  }, `${entry.owner}/${entry.repository}`, h(IconRightUpOutlineRegular, { size: 12 })),
593
- h('div', { className: 'capital-config-skill-tags' }, entry.tags.map((tag) =>
594
- h('span', { key: tag, className: 'capital-config-skill-tag' }, SELECTED_SKILL_TAG_LABELS[locale][tag] ?? tag))),
595
661
  ),
596
- // Capped rows: reason on the icon before the switch; the icon ignores custom
597
- // props, so title / role sit on its wrapper.
598
- capReached && state?.on !== true ? h('span', {
599
- className: 'capital-config-skill-cap-icon',
600
- title: capTooltip,
601
- role: 'img',
602
- 'aria-label': capTooltip,
603
- }, h(IconWarningTriangleOutlineRegular, { size: 14 })) : null,
604
662
  h(Switch, {
605
663
  label,
606
664
  checked: state?.on === true,
607
- // Cap blocks adding only: an enabled row can always be turned off.
608
- disabled: disabled || state?.writing === true || (capReached && state?.on !== true),
665
+ disabled: disabled || state?.writing === true,
609
666
  onChange,
610
667
  }),
611
668
  ),
@@ -623,9 +680,11 @@ function CapitalCard(props) {
623
680
  const t = props.t
624
681
  const state = props.useCapitalCard((snapshot) => snapshot)
625
682
  const disabled = !state.writable
683
+ const autoMode = state.selectedSkillsMode?.auto === true
626
684
  const locale = skillLocale()
627
685
  const [tab, setTab] = useState('sources')
628
- const [tagFilter, setTagFilter] = useState('all')
686
+ const [traitFilter, setTraitFilter] = useState(SELECTED_SKILL_FILTER_ALL)
687
+ const [presetFilter, setPresetFilter] = useState(SELECTED_SKILL_FILTER_ALL)
629
688
  // Fetch on mount: tab labels carry numbers; hooks must precede the unavailable return.
630
689
  useEffect(() => { props.ensureBundleRows() }, [])
631
690
  // Pinned order is snapshotted on tab entry so rows never move under the cursor.
@@ -633,17 +692,11 @@ function CapitalCard(props) {
633
692
  .filter((entry) => state.selectedSkills?.[entry.key]?.on === true)
634
693
  .map((entry) => entry.key))
635
694
  const [pinnedSkills, setPinnedSkills] = useState(enabledKeys)
636
- useEffect(() => { if (state.available) setPinnedSkills(enabledKeys()) }, [state.available])
637
- const tagCounts = new Map()
638
- for (const entry of SELECTED_SKILL_CATALOG) {
639
- for (const tag of entry.tags) tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1)
640
- }
641
- tagCounts.set('all', SELECTED_SKILL_CATALOG.length)
642
- const tags = ['all', ...Array.from(tagCounts.keys())
643
- .filter((tag) => tag !== 'all')
644
- .sort((left, right) => tagCounts.get(right) - tagCounts.get(left))]
645
- const visibleSkills = SELECTED_SKILL_CATALOG
646
- .filter((entry) => tagFilter === 'all' || entry.tags.includes(tagFilter))
695
+ useEffect(() => {
696
+ if (state.available && !autoMode) setPinnedSkills(enabledKeys())
697
+ }, [state.available, autoMode])
698
+ const facetCounts = selectedSkillFacetCounts({ trait: traitFilter, preset: presetFilter })
699
+ const visibleSkills = filterSelectedSkills({ trait: traitFilter, preset: presetFilter })
647
700
  .sort((left, right) => (pinnedSkills.has(left.key) ? 0 : 1) - (pinnedSkills.has(right.key) ? 0 : 1))
648
701
  const form = h(SettingsForm, {
649
702
  labels: {
@@ -679,42 +732,75 @@ function CapitalCard(props) {
679
732
  ),
680
733
  state.localFetch?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
681
734
  )
682
- const tagLabels = SELECTED_SKILL_TAG_LABELS[locale]
683
- const skillsFilter = h('div', { className: 'capital-config-selected-filter', role: 'group', 'aria-label': t('selectedSkillsFilterLabel') },
684
- ...tags.map((tag) => h('button', {
685
- key: tag,
735
+ const modeSection = h('div', { className: 'capital-config-selected-mode' },
736
+ h('div', { className: 'capital-config-switch-row' },
737
+ h('div', { className: 'capital-config-switch-text' },
738
+ h('span', { className: 'capital-config-switch-label' }, t('selectedSkillsModeLabel')),
739
+ h('p', { className: 'capital-config-hint' }, t('selectedSkillsModeHint')),
740
+ ),
741
+ h(Switch, {
742
+ label: t('selectedSkillsModeLabel'),
743
+ checked: autoMode,
744
+ disabled: disabled || state.saving || state.selectedSkillsMode?.writing === true,
745
+ onChange: props.toggleSelectedSkillsMode,
746
+ }),
747
+ ),
748
+ state.selectedSkillsMode?.failed ? h('p', { role: 'status', className: 'capital-config-write-failed' }, t('saveFailed')) : null,
749
+ )
750
+ const traitLabels = SELECTED_SKILL_TAG_LABELS[locale]
751
+ const filterRow = (label, pills) => h('div', { className: 'capital-config-filter-row' },
752
+ h('span', { className: 'capital-config-filter-label' }, label),
753
+ h('div', { className: 'capital-config-filter-options', role: 'group', 'aria-label': label }, ...pills),
754
+ )
755
+ const traitPills = SELECTED_SKILL_TRAITS.map((trait) => h(Pill, {
756
+ key: trait,
757
+ type: 'button',
758
+ className: 'capital-config-filter-pill',
759
+ active: traitFilter === trait,
760
+ 'aria-pressed': traitFilter === trait ? 'true' : 'false',
761
+ onClick: () => setTraitFilter(trait),
762
+ },
763
+ trait === SELECTED_SKILL_FILTER_ALL ? t('selectedSkillsAll') : (traitLabels[trait] ?? trait),
764
+ h('span', { className: 'capital-config-filter-count' }, String(facetCounts.traits.get(trait) ?? 0))))
765
+ const presetPills = [SELECTED_SKILL_FILTER_ALL, ...SELECTED_SKILL_PRESET_ORDER].map((preset) => {
766
+ const total = preset === SELECTED_SKILL_FILTER_ALL
767
+ ? SELECTED_SKILL_CATALOG.length
768
+ : SELECTED_SKILL_PRESET_TOTALS.get(preset) ?? 0
769
+ const unavailable = preset !== SELECTED_SKILL_FILTER_ALL && total === 0
770
+ return h(Pill, {
771
+ key: preset,
686
772
  type: 'button',
687
- className: 'capital-config-selected-filter-button',
688
- 'aria-pressed': tagFilter === tag ? 'true' : 'false',
689
- onClick: () => setTagFilter(tag),
690
- }, tagLabels[tag] ?? tag, h('span', { className: 'capital-config-selected-filter-count' }, String(tagCounts.get(tag))))),
773
+ className: 'capital-config-filter-pill',
774
+ active: presetFilter === preset,
775
+ 'aria-pressed': presetFilter === preset ? 'true' : 'false',
776
+ disabled: unavailable,
777
+ title: unavailable ? t('selectedSkillsPresetUnavailable') : undefined,
778
+ onClick: () => setPresetFilter(preset),
779
+ },
780
+ preset === SELECTED_SKILL_FILTER_ALL ? t('selectedSkillsAll') : presetLabel(preset, locale),
781
+ h('span', { className: 'capital-config-filter-count' }, String(facetCounts.presets.get(preset) ?? 0)))
782
+ })
783
+ const skillsFilter = h('div', { className: 'capital-config-filters', role: 'group', 'aria-label': t('selectedSkillsFilterLabel') },
784
+ filterRow(t('selectedSkillsTraitGroup'), traitPills),
785
+ filterRow(t('selectedSkillsPresetGroup'), presetPills),
691
786
  )
692
- // Live count for the tab and cap. The cap is edit-time only: existing selections keep working.
693
- const enabledSkills = SELECTED_SKILL_CATALOG.filter(
694
- (entry) => state.selectedSkills?.[entry.key]?.on === true,
695
- ).length
696
- const capReached = enabledSkills >= SELECTED_SKILLS_MAX_ENABLED
697
- // Build the shared tooltip once; per-row drift would show different numbers.
698
- const capTooltip = t('selectedSkillsCapTooltip')
699
- .replace('{count}', `${enabledSkills}/${SELECTED_SKILLS_MAX_ENABLED}`)
787
+ // No cap: the user builds the set; the role prompt caps only how many it lists.
700
788
  const skillRows = visibleSkills.length > 0
701
789
  ? visibleSkills.map((entry) => h(SelectedSkillRow, {
702
790
  key: entry.key,
703
- entry,
791
+ entry: { ...entry, roles: selectedSkillRoles(entry) },
704
792
  state: state.selectedSkills?.[entry.key],
705
793
  disabled: disabled || state.saving,
706
- capReached,
707
- capTooltip,
708
794
  onChange: (value) => props.toggleSelectedSkill(entry.key, value),
709
795
  t,
710
796
  }))
711
- : [h('p', { key: 'empty', className: 'capital-config-hint' }, t('selectedSkillsEmpty'))]
797
+ : [h('p', { key: 'empty', className: 'capital-config-selected-empty' }, t('selectedSkillsEmpty'))]
712
798
  // No cap banner: the icon is the only explanation. Unknown count prints no number.
713
799
  const withCount = (label, count) => (Number.isInteger(count) ? `${label} ${count}` : label)
714
800
  const options = [
715
801
  // A dirty draft while Save is off-screen must be announced on the tab.
716
802
  { value: 'sources', label: state.dirty ? t('titleUnsaved') : t('title') },
717
- { value: 'skills', label: `${t('selectedSkillsTitle')} ${enabledSkills}/${SELECTED_SKILL_CATALOG.length}` },
803
+ { value: 'skills', label: t('selectedSkillsTitle') },
718
804
  { value: 'components', label: withCount(t('componentsTitle'), state.components?.count) },
719
805
  ]
720
806
  const panel = (value, children) => h('section', {
@@ -741,12 +827,12 @@ function CapitalCard(props) {
741
827
  },
742
828
  }),
743
829
  panel('sources', [form, localFetchSection]),
744
- panel('skills', [
745
- h('div', { className: 'capital-config-selected-layout' },
830
+ panel('skills', autoMode
831
+ ? [modeSection]
832
+ : [modeSection, h('div', { className: 'capital-config-skills-browser' },
746
833
  skillsFilter,
747
834
  h('div', { className: 'capital-config-selected-list' }, ...skillRows),
748
- ),
749
- ]),
835
+ )]),
750
836
  // The panel is the host's rows list; keep the aria-controls target non-empty via sr-only.
751
837
  panel('components', [h('span', { className: 'capital-config-sr-only' }, t('componentsHint'))]),
752
838
  )
package/config/index.js CHANGED
@@ -59,15 +59,21 @@ const PaddleOcrSchema = z.object({
59
59
 
60
60
  const SelectedSkillsShape = Object.fromEntries(SELECTED_SKILL_CATALOG.map((entry) => [
61
61
  entry.key,
62
- z.boolean().default(false).volatile().description(`Enable ${entry.name} for the Auto mode root Agent (experimental; default off)`),
62
+ z.boolean().default(false).volatile().description(`Enable ${entry.name} for the Capital role(s) that own it (experimental; default off)`),
63
63
  ]))
64
64
  const SelectedSkillsSchema = z.object(SelectedSkillsShape)
65
65
 
66
66
  const SELECTED_SKILL_DEFAULTS = Object.fromEntries(SELECTED_SKILL_CATALOG.map((entry) => [entry.key, false]))
67
67
 
68
+ const SelectedSkillsModeSchema = z.union(['manual', 'auto'])
69
+ .default('manual')
70
+ .volatile()
71
+ .description('manual = 只用用户勾选;auto = 按角色开放全部方法,运行期忽略勾选')
72
+
68
73
  export const Config = z.object({
69
74
  customPersona: z.string().default('').volatile().description('自动模式附加人设文本'),
70
- selectedSkills: SelectedSkillsSchema.default(SELECTED_SKILL_DEFAULTS).description('Explicitly selected community experiments; default off and visible only to the Auto mode root Agent'),
75
+ selectedSkillsMode: SelectedSkillsModeSchema,
76
+ selectedSkills: SelectedSkillsSchema.default(SELECTED_SKILL_DEFAULTS).description('Explicitly selected community methods; default off, each visible only to the Capital role(s) that own it'),
71
77
  fuyaoCredentialRef: z.string().default('FUYAO_API_KEY').volatile().description('Fuyao credentials 引用名'),
72
78
  ftshareCredentialRef: z.string().default('FTSHARE_API_KEY').volatile().description('FTShare(非凸科技)credentials 引用名'),
73
79
  retriever: z.object({
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/config",
3
- "version": "2.10.0",
3
+ "version": "2.10.2",
4
4
  "description": "自动模式的取数凭据与偏好配置行:卡片坐在 @v587d/capital-generation 插件主页的「配置」段,点这一行本身没有表单。",
5
5
  "license": "MIT",
6
6
  "type": "module",