@v587d/capital-generation 2.7.9 → 2.7.10

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.
@@ -9,8 +9,9 @@ const {
9
9
  Modal,
10
10
  writeClipboard,
11
11
  IconRightUpOutlineRegular,
12
+ IconWarningTriangleOutlineRegular,
12
13
  } = require('@deepseek-ai/dsh-client-ui-primitives')
13
- const { SELECTED_SKILL_CATALOG, SELECTED_SKILL_TAG_LABELS } = require('../selected-skills/catalog.js')
14
+ const { SELECTED_SKILL_CATALOG, SELECTED_SKILL_TAG_LABELS, SELECTED_SKILLS_MAX_ENABLED } = require('../selected-skills/catalog.js')
14
15
 
15
16
  /**
16
17
  * 卡片读的是**配置条目 id**,坐的是**组合包自己的页面**——两个不同的字符串,各自钉在不同地方:
@@ -107,6 +108,11 @@ const zh = {
107
108
  selectedSkillsTitle: '精选 Skills',
108
109
  selectedSkillsFilterLabel: '按能力筛选',
109
110
  selectedSkillsEmpty: '没有匹配这个标签的 Skill。',
111
+ // 上限只挡"再勾一个",不挡"已经勾上的生效":老配置多于上限时照常全部生效,只提示收敛
112
+ // (运行期 provider 不按这个数截断,否则就是"卡片上开着 10 个、会话里只剩 6 个"的静默砍功能)。
113
+ // 被挡住的原因只活在**开关左侧那枚三角警示图标**上(悬停 title / 读屏 aria-label 同一串):
114
+ // {count} 是计数占位("6/6"),由代码拼,字典不存数字。
115
+ selectedSkillsCapTooltip: 'Skill已达上限 ({count})。',
110
116
  selectedSkillsCopyName: '复制技能名',
111
117
  selectedSkillsCopied: '已复制',
112
118
  selectedSkillsDetail: '详情',
@@ -146,6 +152,7 @@ const en = {
146
152
  selectedSkillsTitle: 'Selected Skills',
147
153
  selectedSkillsFilterLabel: 'Filter by capability',
148
154
  selectedSkillsEmpty: 'No skills match this tag.',
155
+ selectedSkillsCapTooltip: 'Skill limit reached ({count}).',
149
156
  selectedSkillsCopyName: 'Copy skill name',
150
157
  selectedSkillsCopied: 'Copied',
151
158
  selectedSkillsDetail: 'Details',
@@ -253,7 +260,12 @@ function installStyles() {
253
260
  .capital-config-switch-text { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 4px; }
254
261
  .capital-config-switch-label { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }
255
262
  .capital-config-hint { margin: 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
256
- .capital-config-write-failed { margin: 0; color: var(--dsw-alias-label-error); font-size: 12px; line-height: 18px; }
263
+ /* ⚠️ 语义色只用主题真实定义的 --dsw-alias-state-*:--dsw-alias-label-error 主题里**没定义**,
264
+ var() 判败回继承色 ⇒ light 黑 / dark 白(这枚图标与"保存失败"行实机就是这么错的)。
265
+ warn 色用 --dsw-alias-state-warn-primary(StateDot / Tag 的 warning 同款)。cursor:help 让
266
+ "它有话要说"看得出来;padding 2px + 负 margin 只放大悬停命中区、不占布局(14px 的图标当悬停靶子太小)。 */
267
+ .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; }
268
+ .capital-config-write-failed { margin: 0; color: var(--dsw-alias-state-error-primary); font-size: 12px; line-height: 18px; }
257
269
  /* 标题右侧的两枚外链:官方那一格是 flex-wrap + gap:8px,整组自己收成一个 inline-flex 项,
258
270
  免得标题变长时被换行拆成两截。颜色只走 --dsw-* 变量,明暗两套都跟着宿主。 */
259
271
  .capital-config-plugin-links { flex: none; align-items: center; gap: 2px; display: inline-flex; }
@@ -570,7 +582,7 @@ function loadSkillDoc(name) {
570
582
  /** 「已复制」亮多久:要够读完,又不能亮到让人以为它是个状态开关。 */
571
583
  const COPIED_FLASH_MS = 1600
572
584
 
573
- function SelectedSkillRow({ entry, state, disabled, onChange, t }) {
585
+ function SelectedSkillRow({ entry, state, disabled, capReached = false, capTooltip, onChange, t }) {
574
586
  const locale = skillLocale()
575
587
  const label = entry.label[locale]
576
588
  const repositoryUrl = `https://github.com/${entry.owner}/${entry.repository}`
@@ -667,10 +679,22 @@ function SelectedSkillRow({ entry, state, disabled, onChange, t }) {
667
679
  h('div', { className: 'capital-config-skill-tags' }, entry.tags.map((tag) =>
668
680
  h('span', { key: tag, className: 'capital-config-skill-tag' }, SELECTED_SKILL_TAG_LABELS[locale][tag] ?? tag))),
669
681
  ),
682
+ // 被上限挡住的行(到上限 + 没勾):开关**左边**一枚三角警示图标,原因挂在图标的
683
+ // title / aria-label 上(同一串,悬停才见)——开关上不挂 title,列表顶上也没有整行提示。
684
+ // 图标组件不吃自定义 props(artwork 只解构 size/className/strokeWidth),所以 title /
685
+ // aria 挂在包着它的 span 上;`role=img` 让读屏在开关之前先听到"为什么它拨不动"。
686
+ capReached && state?.on !== true ? h('span', {
687
+ className: 'capital-config-skill-cap-icon',
688
+ title: capTooltip,
689
+ role: 'img',
690
+ 'aria-label': capTooltip,
691
+ }, h(IconWarningTriangleOutlineRegular, { size: 14 })) : null,
670
692
  h(Switch, {
671
693
  label,
672
694
  checked: state?.on === true,
673
- disabled: disabled || state?.writing === true,
695
+ // 上限只挡"再勾一个":**已勾上的行永远关得掉**——把用户关进一个既不能再选、也不能取消的
696
+ // 格子里,比不给上限糟得多。被挡住的原因由左边那枚警示图标回答(控件不许对自己的状态说谎)。
697
+ disabled: disabled || state?.writing === true || (capReached && state?.on !== true),
674
698
  onChange,
675
699
  }),
676
700
  ),
@@ -774,19 +798,29 @@ function CapitalCard(props) {
774
798
  onClick: () => setTagFilter(tag),
775
799
  }, tagLabels[tag] ?? tag, h('span', { className: 'capital-config-selected-filter-count' }, String(tagCounts.get(tag))))),
776
800
  )
801
+ // 已选数按开关现算(tab 上那个 n/68 也用它)。上限**只在编辑期**生效:它挡"再勾一个",
802
+ // 不挡"已经勾上的生效"——老配置超过上限时运行期照常全部加载,这里只提示收敛。
803
+ const enabledSkills = SELECTED_SKILL_CATALOG.filter(
804
+ (entry) => state.selectedSkills?.[entry.key]?.on === true,
805
+ ).length
806
+ const capReached = enabledSkills >= SELECTED_SKILLS_MAX_ENABLED
807
+ // 被挡住的行共用同一串悬停文案(含计数),拼一次传下去:62 行各自重拼,漂移就是两处对不齐。
808
+ const capTooltip = t('selectedSkillsCapTooltip')
809
+ .replace('{count}', `${enabledSkills}/${SELECTED_SKILLS_MAX_ENABLED}`)
777
810
  const skillRows = visibleSkills.length > 0
778
811
  ? visibleSkills.map((entry) => h(SelectedSkillRow, {
779
812
  key: entry.key,
780
813
  entry,
781
814
  state: state.selectedSkills?.[entry.key],
782
815
  disabled: disabled || state.saving,
816
+ capReached,
817
+ capTooltip,
783
818
  onChange: (value) => props.toggleSelectedSkill(entry.key, value),
784
819
  t,
785
820
  }))
786
821
  : [h('p', { key: 'empty', className: 'capital-config-hint' }, t('selectedSkillsEmpty'))]
787
- const enabledSkills = SELECTED_SKILL_CATALOG.filter(
788
- (entry) => state.selectedSkills?.[entry.key]?.on === true,
789
- ).length
822
+ // 到上限的提示不再占一整行:原因逐行挂在被挡住的开关左侧那枚三角警示图标上(SelectedSkillRow),
823
+ // 这里不画任何汇总提示——一行红字 + 62 个灰开关是两套说法,只留图标一套。
790
824
  // 行数取不到就**只留标题**:把"还不知道"画成 `0` 是控件对自己的状态说谎(数字缺席本身就是信号)。
791
825
  const withCount = (label, count) => (Number.isInteger(count) ? `${label} ${count}` : label)
792
826
  const options = [
@@ -821,10 +855,12 @@ function CapitalCard(props) {
821
855
  },
822
856
  }),
823
857
  panel('sources', [form, localFetchSection]),
824
- panel('skills', [h('div', { className: 'capital-config-selected-layout' },
825
- skillsFilter,
826
- h('div', { className: 'capital-config-selected-list' }, ...skillRows),
827
- )]),
858
+ panel('skills', [
859
+ h('div', { className: 'capital-config-selected-layout' },
860
+ skillsFilter,
861
+ h('div', { className: 'capital-config-selected-list' }, ...skillRows),
862
+ ),
863
+ ]),
828
864
  // 第三块的内容是宿主自己画的那段清单(切过来才在下方显形)。画面上不再写那句"清单在下方"——
829
865
  // 用户点这一格看到的就是清单,多余的一行字只是噪音。但 `aria-controls` 指的这块面板不许是空的
830
866
  // (空面板不报错,只有读屏用户付账),所以那句话改挂在 sr-only 上:零布局贡献、读得到。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-config",
3
- "version": "2.7.9",
3
+ "version": "2.7.10",
4
4
  "description": "Capital 模式的取数凭据与偏好配置行:卡片坐在 @v587d/capital-generation 插件主页的「配置」段,点这一行本身没有表单。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-watchlist",
3
- "version": "2.7.9",
3
+ "version": "2.7.10",
4
4
  "description": "Capital 模式的用户自选股(宿主级清单 + Fuyao 报价快照 + /capital-watchlist 路由 + 输入框菜单弹窗):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-charts",
3
- "version": "2.7.9",
3
+ "version": "2.7.10",
4
4
  "description": "Capital 模式的图表前端组件(浏览器半边 + 序列旁路路由):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,16 @@ export declare const SELECTED_SKILL_CATALOG: readonly SelectedSkillCatalogEntry[
13
13
  export type SelectedSkillKey = typeof SELECTED_SKILL_CATALOG[number]['key'];
14
14
  export type SelectedSkillSelection = Partial<Record<SelectedSkillKey, boolean>>;
15
15
  export type SelectedSkillSelectionSource = SelectedSkillSelection | (() => SelectedSkillSelection);
16
+ type RuntimeSummary = {
17
+ name: string;
18
+ description: string;
19
+ };
20
+ /**
21
+ * 索引块正文。**唯一实现**:条目集合来自 catalog 的 `selectedSkillsForTask`(与目录、闸门同源),
22
+ * 描述用**运行时那一份**(上游 frontmatter,也就是会话目录里模型看到的那句话),不是设置卡片里
23
+ * 策展过的中文摘要——同一个 skill 在同一段上下文里出现两种描述,只会让模型两处对照。
24
+ */
25
+ export declare function renderTaskSkillIndex(taskId: string, summaries: readonly RuntimeSummary[]): string | undefined;
16
26
  type FileSystemSkillConfig = {
17
27
  providerName: string;
18
28
  includeDefaultRoots: false;
@@ -1,10 +1,12 @@
1
1
  import { FileSystemSkillProvider } from '@deepseek-ai/dsh-skill-filesystem';
2
+ import { readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import { fileURLToPath } from 'node:url';
3
5
  import { agentSessionId, isRootAgent } from './root-tool-policy.js';
4
6
  // The catalog is a shipped data file outside the TypeScript root; runtime keeps this
5
7
  // import so the published lib resolves the same metadata beside the pinned snapshots.
6
8
  // @ts-expect-error catalog.js is intentionally maintained as package data.
7
- import { SELECTED_SKILL_CATALOG as CATALOG, SELECTED_SKILL_SKILL_DIRS } from '../../selected-skills/catalog.js';
9
+ import { SELECTED_SKILL_CATALOG as CATALOG, SELECTED_SKILL_SKILL_DIRS, SELECTED_SKILL_TASK_IDS, selectedSkillsForTask } from '../../selected-skills/catalog.js';
8
10
  export const SELECTED_SKILL_CATALOG = CATALOG;
9
11
  const PROVIDER_NAME = 'capital-selected-skills';
10
12
  /** 本 preset 的身份:两条注册路径(`agent/created` 与 `agent-preset/selected`)共用一份。 */
@@ -13,6 +15,86 @@ const CAPITAL_PRESET_ID = 'capital-generation';
13
15
  // plugins/us-stock-analysis/skills,不在 <repo>/skills 底下。写死目录列表的表现不是报错,
14
16
  // 而是"用户在卡片上勾了那一格、技能永远不出现"(AGENTS.md §9.7)。
15
17
  const SKILL_ROOTS = Object.entries(SELECTED_SKILL_SKILL_DIRS).map(([repository, skillsDir]) => fileURLToPath(new URL(`../../selected-skills/${repository}/${skillsDir}`, import.meta.url)));
18
+ /**
19
+ * 任务型内置 skill 的根目录(`lib/agents/` → 包根 → `preset/capital-generation/skills`)。
20
+ *
21
+ * 索引**不改磁盘**:`preset/.../SKILL.md` 一个字节都不动,正文在 `get()` 读取期拼上索引块。
22
+ * 三个理由,缺一都不是洁癖:装好的包常常整棵只读(全局安装更糟);`skill-filesystem` 的 watcher
23
+ * 会被写文件搅动,多会话并行时互相打架;`test/persona.test.mjs` 正钉着这些文件的 frontmatter 与
24
+ * 体积,运行期改写会让那些闸门失去意义。
25
+ */
26
+ const TASK_SKILL_ROOT = fileURLToPath(new URL('../../preset/capital-generation/skills', import.meta.url));
27
+ /**
28
+ * 遮蔽候选的 rank。DSH 的 `compareIndexedCandidates` 先比 rank 再比 provider 顺序,**小的赢**;
29
+ * 同层自定义根是 `CUSTOM_RANK = 300`,这里取 150 保证同层时压过 fs provider。
30
+ *
31
+ * 跨层本来也轮不到 rank:`dsh-agent-preset-registry` 把 preset 的 standing scope 挂成
32
+ * **agent scope 的 parent**(`standingKey = scopeParentOf(agentKey)`),而 `chainLayers` 的注释写明
33
+ * "farthest ancestor first, the nearest scope has the final word"、`scopeChainOf(...).reverse()` 后
34
+ * 逐层 `merged.set(name, ...)` —— 本 provider 注册在 agent scope 里,是最近的一层,同名天然覆盖。
35
+ * 两条机制取其一即可,取两条是因为遮蔽失败的症状是"索引静默消失",没有第二个人会发现。
36
+ */
37
+ const TASK_SKILL_SHADOW_RANK = 150;
38
+ const INDEX_HEADING = '## 附:本轮可用的外部精选 Skill(索引)';
39
+ /**
40
+ * 索引块的框。它把**外部方法论**塞进了**宿主契约 skill** 的正文里,框架被拉平了,所以这四句必须
41
+ * 写在块内而不是别的文档里:冲突判据、不可动摇的三处边界、以及"这里只有摘要"。
42
+ */
43
+ const INDEX_FRAME = [
44
+ '以下是你(用户)在设置里启用的外部精选 Skill,按能力标签与本次任务关联后列出。它们是**方法论材料**,',
45
+ '需要正文时照常用 `skill` 工具按名加载。它们**不改变**本 skill 的证据门、工具委派路径、输出通道与报告形状;',
46
+ '与本 skill 冲突时**以本 skill 为准**。这里只给名称与摘要,摘要不等于全文,未加载前不要照它行事。',
47
+ ];
48
+ /** 最小 frontmatter 读取:只要 `name` / `description` / 正文。完整策略解析仍归 fs provider。 */
49
+ function readFrontmatter(raw) {
50
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
51
+ if (match === null)
52
+ return undefined;
53
+ const fields = new Map();
54
+ for (const line of match[1].split(/\r?\n/)) {
55
+ const field = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
56
+ if (field !== null)
57
+ fields.set(field[1], field[2].trim());
58
+ }
59
+ return { fields, body: raw.slice(match[0].length) };
60
+ }
61
+ /**
62
+ * 载入三颗任务型 skill 的正文。**读不到就不遮蔽**:`list()` 不会列出这个候选,fs provider 照常
63
+ * 供原文,用户看到的是"没有索引",而不是"这颗 skill 不见了"(AGENTS.md §9.7 ⑤ 失败要留余地)。
64
+ */
65
+ function loadTaskSkillBases(root = TASK_SKILL_ROOT) {
66
+ const bases = new Map();
67
+ for (const name of SELECTED_SKILL_TASK_IDS) {
68
+ try {
69
+ const raw = readFileSync(join(root, name, 'SKILL.md'), 'utf8');
70
+ const parsed = readFrontmatter(raw);
71
+ const description = parsed?.fields.get('description');
72
+ if (parsed === undefined || description === undefined || description === '')
73
+ continue;
74
+ bases.set(name, { name, description, content: parsed.body.trim(), directory: join(root, name) });
75
+ }
76
+ catch {
77
+ // 读不到是 fail-open,不是错误路径:默认态(一颗都没启用)本来就不遮蔽任何东西。
78
+ }
79
+ }
80
+ return bases;
81
+ }
82
+ /**
83
+ * 索引块正文。**唯一实现**:条目集合来自 catalog 的 `selectedSkillsForTask`(与目录、闸门同源),
84
+ * 描述用**运行时那一份**(上游 frontmatter,也就是会话目录里模型看到的那句话),不是设置卡片里
85
+ * 策展过的中文摘要——同一个 skill 在同一段上下文里出现两种描述,只会让模型两处对照。
86
+ */
87
+ export function renderTaskSkillIndex(taskId, summaries) {
88
+ if (summaries.length === 0)
89
+ return undefined;
90
+ return [
91
+ INDEX_HEADING,
92
+ '',
93
+ ...INDEX_FRAME,
94
+ '',
95
+ ...summaries.map((entry) => `- \`${entry.name}\`: ${entry.description}`),
96
+ ].join('\n');
97
+ }
16
98
  function createFilesystemSkillProvider(ctx, control, config) {
17
99
  return new FileSystemSkillProvider(ctx, control, config);
18
100
  }
@@ -95,21 +177,71 @@ export function registerRootSelectedSkills(ctx, selection, createFilesystemProvi
95
177
  customSkillDirs: SKILL_ROOTS,
96
178
  watch: true,
97
179
  });
180
+ // 遮蔽用的底稿只在注册时读一次(三份小文件,读不到就不遮蔽)。
181
+ const taskBases = loadTaskSkillBases();
182
+ // 上一次 `list()` 看到的"已启用精选"运行时摘要。`get()` 拼索引时读它,避免为了几行摘要
183
+ // 再枚举一遍快照目录;为空就退回"没有索引",也就是把整颗让给 fs provider 的原文。
184
+ let runtimeSummaries = [];
185
+ const indexFor = (taskId) => {
186
+ const byName = new Map(runtimeSummaries.map((entry) => [entry.name, entry]));
187
+ // 条目集合来自 catalog 的唯一实现(与目录、闸门同源),顺序取 catalog 顺序:
188
+ // 同一仓库的几颗排在一起,读起来比按名字打散更像一份索引。
189
+ const summaries = selectedSkillsForTask(taskId, [...byName.keys()])
190
+ .map((entry) => byName.get(entry.name))
191
+ .filter((entry) => entry !== undefined);
192
+ return renderTaskSkillIndex(taskId, summaries);
193
+ };
98
194
  const provider = {
99
195
  name: PROVIDER_NAME,
100
196
  async list(options) {
101
197
  // 调用期现读:开关是 settings 偏好,不许在 Agent 创建时定格。
102
198
  const enabled = selectedNames(readSelection(selection));
103
199
  // 全关是默认常态:短路掉,不碰快照目录(filesystem watcher 在它的 list()
104
- // 里才启动,短路保证默认关闭的会话不起任何 watcher)。
105
- if (enabled.size === 0)
200
+ // 里才启动,短路保证默认关闭的会话不起任何 watcher),**也不遮蔽任何内置 skill**:
201
+ // 一颗都没启用时用户看到的必须与没有这个 provider 时逐字相同。
202
+ if (enabled.size === 0) {
203
+ runtimeSummaries = [];
106
204
  return [];
205
+ }
107
206
  const result = await filesystemProvider.list(options);
108
- if (Array.isArray(result))
109
- return result.filter((candidate) => enabled.has(candidate.name));
110
- return { ...result, candidates: result.candidates.filter((candidate) => enabled.has(candidate.name)) };
207
+ const candidates = Array.isArray(result) ? result : result.candidates;
208
+ const selected = candidates.filter((candidate) => enabled.has(candidate.name));
209
+ runtimeSummaries = selected.map((candidate) => ({ name: candidate.name, description: candidate.description }));
210
+ // 任务型内置 skill 的遮蔽候选:只有当这颗**确实有索引可给**时才列出来。
211
+ // 没有索引就整颗让给 fs provider(正文照旧),因此"索引缺失"永远不会演变成
212
+ // "这颗 skill 不见了"——遮蔽是加法,不是替换。
213
+ const shadows = [...taskBases.values()]
214
+ .filter((base) => indexFor(base.name) !== undefined)
215
+ .map((base) => ({
216
+ name: base.name,
217
+ description: base.description,
218
+ invocation: { modelInvocable: true, userInvocable: true },
219
+ provider: PROVIDER_NAME,
220
+ source: 'custom',
221
+ rank: TASK_SKILL_SHADOW_RANK,
222
+ locator: { kind: 'task-skill', name: base.name },
223
+ resourceBase: { kind: 'directory', path: base.directory },
224
+ }));
225
+ const merged = [...selected, ...shadows];
226
+ return Array.isArray(result) ? merged : { ...result, candidates: merged };
111
227
  },
112
228
  get(candidate, options) {
229
+ const base = taskBases.get(candidate.name);
230
+ if (base !== undefined) {
231
+ // 索引在**读取期**算:开关是 settings 偏好,加载这一刻才是最新的。
232
+ const index = indexFor(base.name);
233
+ const content = index === undefined ? base.content : `${base.content}\n\n${index}`;
234
+ return Promise.resolve({
235
+ name: base.name,
236
+ description: base.description,
237
+ invocation: { modelInvocable: true, userInvocable: true },
238
+ source: 'custom',
239
+ provider: PROVIDER_NAME,
240
+ resourceBase: { kind: 'directory', path: base.directory },
241
+ path: join(base.directory, 'SKILL.md'),
242
+ content,
243
+ });
244
+ }
113
245
  if (!selectedNames(readSelection(selection)).has(candidate.name)) {
114
246
  throw new Error(`selected skill is not enabled: ${candidate.name}`);
115
247
  }
@@ -6,7 +6,7 @@
6
6
  * between validation and connect. Closing that window would require a custom
7
7
  * dispatcher; this module keeps the same explicit trade-off as dsh-search-first.
8
8
  */
9
- export declare const LOCAL_FETCH_CLIENT_VERSION = "2.7.9";
9
+ export declare const LOCAL_FETCH_CLIENT_VERSION = "2.7.10";
10
10
  export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
11
11
  export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
12
12
  export declare class LocalFetchError extends Error {
@@ -7,7 +7,7 @@
7
7
  * dispatcher; this module keeps the same explicit trade-off as dsh-search-first.
8
8
  */
9
9
  import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
10
- export const LOCAL_FETCH_CLIENT_VERSION = '2.7.9';
10
+ export const LOCAL_FETCH_CLIENT_VERSION = '2.7.10';
11
11
  export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
12
12
  export class LocalFetchError extends Error {
13
13
  code;
@@ -52,7 +52,7 @@ export interface WindCallResult {
52
52
  * `test/chart-composition.test.mjs` 守着(会因漂移而失败),这里原本是写死的第二处,
53
53
  * 一次发版就得记得改两遍。所以本常量由测试一并核对(见 `test/wind-client.test.mjs`)。
54
54
  */
55
- export declare const WIND_CLIENT_VERSION = "2.7.9";
55
+ export declare const WIND_CLIENT_VERSION = "2.7.10";
56
56
  export interface WindClient {
57
57
  callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
58
58
  }
@@ -35,7 +35,7 @@ export const WIND_SERVER_ENDPOINTS = {
35
35
  * `test/chart-composition.test.mjs` 守着(会因漂移而失败),这里原本是写死的第二处,
36
36
  * 一次发版就得记得改两遍。所以本常量由测试一并核对(见 `test/wind-client.test.mjs`)。
37
37
  */
38
- export const WIND_CLIENT_VERSION = '2.7.9';
38
+ export const WIND_CLIENT_VERSION = '2.7.10';
39
39
  function errorText(error) {
40
40
  return error instanceof Error ? error.message : String(error);
41
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-generation",
3
- "version": "2.7.9",
3
+ "version": "2.7.10",
4
4
  "description": "Capital Generation:面向中国散户的 AI 投资助手插件(Capital mode Agent Preset)",
5
5
  "icon": "assets/cg-icon.png",
6
6
  "author": "v587d",
@@ -28,8 +28,9 @@ export const SELECTED_SKILL_SKILL_DIRS = Object.freeze({
28
28
  })
29
29
 
30
30
  // Selected-skill metadata owned by Capital Generation. Upstream snapshots remain unchanged.
31
- // 能力标签的词表就是下面 SELECTED_SKILL_TAG_LABELS 的键:卡片按条目实际用到的标签现算筛选条,
32
- // 少一套语言的显示名就会露出英文原词(test/capital-config.test.mjs 钉着)。
31
+ // 能力标签的词表由文件末尾的 SELECTED_SKILL_TAGS 注册表现算派生(SELECTED_SKILL_TAG_LABELS):
32
+ // 卡片按条目实际用到的标签现算筛选条,少一套语言的显示名就会露出英文原词(test/capital-config.test.mjs
33
+ // 钉着);tags 同时决定条目进哪几颗任务型内置 skill 的索引,所以登记一处、两处生效。
33
34
  export const SELECTED_SKILL_CATALOG = Object.freeze([
34
35
  {
35
36
  key: 'buffettFramework',
@@ -1329,59 +1330,140 @@ export const SELECTED_SKILL_CATALOG = Object.freeze([
1329
1330
  },
1330
1331
  ])
1331
1332
 
1333
+ // ── 能力标签注册表:索引与筛选的唯一来源 ─────────────────────────────────────────
1334
+ //
1335
+ // 一条精选 skill 的 tags 同时决定两件事:① 设置卡片左侧筛选条怎么分类它;② 它进哪几颗
1336
+ // **任务型内置 skill** 的索引。所以标签不是"展示分类",而是"路由声明"——登记一次,两处生效。
1337
+ //
1338
+ // 新增一个标签 = 在 SELECTED_SKILL_TAGS 里加一行(zh / en 本来就必须填,`capital-config.test.mjs`
1339
+ // 钉着),`serves` 或 `fallback` 顺带填掉。没有第二处要改,也没有任何东西会因为"忘了挂"而静默变成孤儿:
1340
+ // 下面四条闸门(`test/selected-skill-tags.test.mjs`)会把漏挂、拼错、孤儿和上限都打红。
1341
+
1342
+ /**
1343
+ * 任务型内置 skill:正文会被补上"外部精选 skill 索引"的那几颗(住 `preset/capital-generation/skills/`)。
1344
+ * 其余五颗内置 skill 是协议 / 流程类(data / web / chart / visualization / orchestration),读者是
1345
+ * 特定角色或特定工具调用,不是"用户提了一个研究任务"的入口,因此不接索引。
1346
+ */
1347
+ export const SELECTED_SKILL_TASK_IDS = Object.freeze([
1348
+ 'capital-equity-research',
1349
+ 'capital-event-impact',
1350
+ 'capital-portfolio-review',
1351
+ ])
1352
+
1353
+ /** 没有任何归属的标签,其条目兜底进这一颗(2026-10-13 决策:一切兜底都往 equity 里装)。 */
1354
+ export const SELECTED_SKILL_FALLBACK_TASK = 'capital-equity-research'
1355
+
1356
+ /**
1357
+ * 标签轴。`routes: false` 的轴只做**标注**,不决定条目进哪颗任务型 skill;`routes: true` 的轴参与路由。
1358
+ *
1359
+ * 市场口径轴是**不路由**的,这一点被实测纠过一次:最初把它写成"对所有任务型 skill 生效",结果
1360
+ * 你这 10 个启用项(9 颗带 `a-share`)在三颗任务型 skill 里得到**完全相同**的清单,索引等于失效;
1361
+ * 修成不路由之后,`10k-digest`(美股 10-K 深读)只进 equity / event-impact,`tax-lens` 只进
1362
+ * portfolio。判据因此是:"是 A 股"不蕴含"服务单股研究 / 事件影响 / 组合复盘"。
1363
+ *
1364
+ * 好处顺带落在自动化上:**新增一个市场口径标签零决策**——不写 serves、不重跑任何分配,
1365
+ * 只登记一行(zh/en/axis)即可。新增一个 routing 轴 = 这里加一行,该轴的标签各自带 `axis`。
1366
+ */
1367
+ export const SELECTED_SKILL_TAG_AXES = Object.freeze({
1368
+ market: Object.freeze({ routes: false }),
1369
+ capability: Object.freeze({ routes: true }),
1370
+ topic: Object.freeze({ routes: true }),
1371
+ })
1372
+
1373
+ /**
1374
+ * 能力标签注册表。键集 ≡ 标签词表(`SELECTED_SKILL_TAG_LABELS` 由它派生)。三轴判据:
1375
+ * market —— 什么市场口径(A 股 / 美股 / 将来的港股…),**不路由**,只做标注;
1376
+ * capability —— 调用什么分析方法(估值 / 财务健康 / 证据核验…);
1377
+ * topic —— 落在什么场景或对象上(组合、监控、税务…)。
1378
+ * routing 轴(capability / topic)的 `serves` 与 `fallback` 二选一:前者点名接收它的任务型 skill
1379
+ * (允许重叠),后者是"还没想好挂哪颗"的显式承认。两者都不给 = 孤儿,闸门会红——不写成"静默不出现",
1380
+ * 因为静默正是这套东西最容易坏的地方。
1381
+ */
1382
+ export const SELECTED_SKILL_TAGS = Object.freeze({
1383
+ 'a-share': { zh: 'A 股', en: 'A-share', axis: 'market', serves: [] },
1384
+ 'business-analysis': { zh: '业务分析', en: 'Business analysis', axis: 'capability', serves: ['capital-equity-research'] },
1385
+ 'capital-allocation': { zh: '资本配置', en: 'Capital allocation', axis: 'capability', serves: ['capital-equity-research'] },
1386
+ 'cash-flow': { zh: '现金流', en: 'Cash flow', axis: 'capability', serves: ['capital-equity-research', 'capital-event-impact'] },
1387
+ competition: { zh: '竞争格局', en: 'Competition', axis: 'topic', serves: ['capital-equity-research'] },
1388
+ earnings: { zh: '业绩与电话会', en: 'Earnings', axis: 'capability', serves: ['capital-equity-research', 'capital-event-impact'] },
1389
+ // `education` 是这张表里最勉强的一条:post-trade-review / journal-pattern-analyzer 确实服务组合复盘,
1390
+ // learning-coach 则哪边都不像。归到 portfolio 是先给一个明确归属,而不是让它掉进兜底。
1391
+ education: { zh: '教学', en: 'Education', axis: 'topic', serves: ['capital-portfolio-review'] },
1392
+ filings: { zh: '披露文件', en: 'Filings', axis: 'capability', serves: ['capital-equity-research', 'capital-event-impact'] },
1393
+ 'financial-health': { zh: '财务健康', en: 'Financial health', axis: 'capability', serves: ['capital-equity-research', 'capital-event-impact'] },
1394
+ instruments: { zh: '期权与基金', en: 'Options & funds', axis: 'topic', serves: ['capital-portfolio-review'] },
1395
+ macro: { zh: '宏观与行业', en: 'Macro & industry', axis: 'topic', serves: ['capital-equity-research', 'capital-event-impact'] },
1396
+ monitoring: { zh: '监控', en: 'Monitoring', axis: 'topic', serves: ['capital-event-impact', 'capital-portfolio-review'] },
1397
+ orders: { zh: '订单', en: 'Orders', axis: 'topic', serves: ['capital-equity-research'] },
1398
+ perspective: { zh: '分析视角', en: 'Perspective', axis: 'topic', serves: ['capital-equity-research', 'capital-event-impact'] },
1399
+ 'peter-lynch': { zh: '彼得·林奇', en: 'Peter Lynch', axis: 'topic', serves: ['capital-equity-research'] },
1400
+ portfolio: { zh: '组合与仓位', en: 'Portfolio', axis: 'topic', serves: ['capital-portfolio-review'] },
1401
+ risk: { zh: '风险', en: 'Risk', axis: 'topic', serves: ['capital-equity-research', 'capital-event-impact', 'capital-portfolio-review'] },
1402
+ strategy: { zh: '战略', en: 'Strategy', axis: 'topic', serves: ['capital-equity-research'] },
1403
+ tax: { zh: '税务', en: 'Tax', axis: 'topic', serves: ['capital-portfolio-review'] },
1404
+ technicals: { zh: '技术面', en: 'Technicals', axis: 'topic', serves: ['capital-event-impact', 'capital-portfolio-review'] },
1405
+ 'trade-discipline': { zh: '交易纪律', en: 'Trade discipline', axis: 'topic', serves: ['capital-portfolio-review'] },
1406
+ 'us-stock': { zh: '美股', en: 'US equity', axis: 'market', serves: [] },
1407
+ valuation: { zh: '估值', en: 'Valuation', axis: 'capability', serves: ['capital-equity-research', 'capital-event-impact'] },
1408
+ verification: { zh: '证据核验', en: 'Verification', axis: 'capability', serves: ['capital-equity-research', 'capital-event-impact', 'capital-portfolio-review'] },
1409
+ })
1410
+
1411
+ /**
1412
+ * 设置卡片最多能同时勾选的精选 skill 数。**只约束"能勾几个",不约束"已勾的生效"**:
1413
+ * 运行期 provider 不按这个数截断——老配置多于 6 个时照常全部生效并提示收敛,否则就是
1414
+ * "卡片上开着 10 个、会话里只有 6 个"那种静默砍功能(AGENTS.md §9.7)。
1415
+ */
1416
+ export const SELECTED_SKILLS_MAX_ENABLED = 6
1417
+
1418
+ /** 标签的显示名(含恒在第一格的「全部」)。键集恒等于注册表,所以新标签只登记一次。 */
1332
1419
  export const SELECTED_SKILL_TAG_LABELS = Object.freeze({
1333
- zh: {
1420
+ zh: Object.freeze({
1334
1421
  all: '全部',
1335
- 'a-share': 'A 股',
1336
- 'business-analysis': '业务分析',
1337
- 'capital-allocation': '资本配置',
1338
- 'cash-flow': '现金流',
1339
- competition: '竞争格局',
1340
- earnings: '业绩与电话会',
1341
- education: '教学',
1342
- filings: '披露文件',
1343
- 'financial-health': '财务健康',
1344
- instruments: '期权与基金',
1345
- macro: '宏观与行业',
1346
- monitoring: '监控',
1347
- orders: '订单',
1348
- perspective: '分析视角',
1349
- 'peter-lynch': '彼得·林奇',
1350
- portfolio: '组合与仓位',
1351
- risk: '风险',
1352
- strategy: '战略',
1353
- tax: '税务',
1354
- technicals: '技术面',
1355
- 'trade-discipline': '交易纪律',
1356
- 'us-stock': '美股',
1357
- valuation: '估值',
1358
- verification: '证据核验',
1359
- },
1360
- en: {
1422
+ ...Object.fromEntries(Object.entries(SELECTED_SKILL_TAGS).map(([tag, meta]) => [tag, meta.zh])),
1423
+ }),
1424
+ en: Object.freeze({
1361
1425
  all: 'All',
1362
- 'a-share': 'A-share',
1363
- 'business-analysis': 'Business analysis',
1364
- 'capital-allocation': 'Capital allocation',
1365
- 'cash-flow': 'Cash flow',
1366
- competition: 'Competition',
1367
- earnings: 'Earnings',
1368
- education: 'Education',
1369
- filings: 'Filings',
1370
- 'financial-health': 'Financial health',
1371
- instruments: 'Options & funds',
1372
- macro: 'Macro & industry',
1373
- monitoring: 'Monitoring',
1374
- orders: 'Orders',
1375
- perspective: 'Perspective',
1376
- 'peter-lynch': 'Peter Lynch',
1377
- portfolio: 'Portfolio',
1378
- risk: 'Risk',
1379
- strategy: 'Strategy',
1380
- tax: 'Tax',
1381
- technicals: 'Technicals',
1382
- 'trade-discipline': 'Trade discipline',
1383
- 'us-stock': 'US equity',
1384
- valuation: 'Valuation',
1385
- verification: 'Verification',
1386
- },
1426
+ ...Object.fromEntries(Object.entries(SELECTED_SKILL_TAGS).map(([tag, meta]) => [tag, meta.en])),
1427
+ }),
1387
1428
  })
1429
+
1430
+ /** 该标签服务的任务型 skill(不路由的轴 → 空;未登记的标签 → 空)。 */
1431
+ export function taggedTaskSkills(tag) {
1432
+ const meta = SELECTED_SKILL_TAGS[tag]
1433
+ if (meta === undefined) return []
1434
+ const axis = SELECTED_SKILL_TAG_AXES[meta.axis]
1435
+ if (axis === undefined || !axis.routes) return []
1436
+ return [...meta.serves]
1437
+ }
1438
+
1439
+ /** 该任务型 skill 收到的全部标签。派生自注册表:反向可达闸门与文档读它,不手写清单。 */
1440
+ export function taskSkillTags(taskId) {
1441
+ return Object.keys(SELECTED_SKILL_TAGS).filter((tag) => taggedTaskSkills(tag).includes(taskId))
1442
+ }
1443
+
1444
+ /** 该条目是否挂上了至少一颗任务型 skill——没挂上的按兜底规则进 `SELECTED_SKILL_FALLBACK_TASK`。 */
1445
+ function entryRoutesAnywhere(entry) {
1446
+ return entry.tags.some((tag) => taggedTaskSkills(tag).length > 0)
1447
+ }
1448
+
1449
+ /**
1450
+ * **唯一实现**:已启用(按运行时 skill name)的精选条目里,某颗任务型 skill 应当索引到哪几条。
1451
+ * 目录(provider 的 `list()`)、任务型 skill 的索引块、测试三处都调这一个函数,不许各自重算
1452
+ * (AGENTS.md §9.7:两份实现必然漂移,而漂移的表现是"某一处少了/多了一条",零报错)。
1453
+ * `enabledNames` 由调用方在**调用期**现读开关得到——开关是 settings 偏好,不许在装配期定格。
1454
+ */
1455
+ export function selectedSkillsForTask(taskId, enabledNames) {
1456
+ const enabled = new Set(enabledNames)
1457
+ return SELECTED_SKILL_CATALOG.filter((entry) => {
1458
+ if (!enabled.has(entry.name)) return false
1459
+ if (entry.tags.some((tag) => taggedTaskSkills(tag).includes(taskId))) return true
1460
+ // 兜底只在 equity 生效(2026-10-13 决策):一条都挂不上的条目,宁可多给它看一条,
1461
+ // 也不要静默不出现。`fallback: true` 的标签走的就是这条路。
1462
+ return taskId === SELECTED_SKILL_FALLBACK_TASK && !entryRoutesAnywhere(entry)
1463
+ })
1464
+ }
1465
+
1466
+ /** 只靠兜底才看得见的条目(当前应为空)。闸门钉住它:新增孤儿会当场点名,而不是静默消失。 */
1467
+ export function fallbackOnlySkillNames() {
1468
+ return SELECTED_SKILL_CATALOG.filter((entry) => !entryRoutesAnywhere(entry)).map((entry) => entry.name)
1469
+ }