@v587d/capital-generation 2.9.2 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +8 -5
  3. package/chart-ui/package.json +1 -1
  4. package/config/client.js +8 -6
  5. package/config/client.src.cjs +74 -34
  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/settled-wake.d.ts +43 -0
  11. package/lib/agents/settled-wake.js +81 -0
  12. package/lib/agents/skill-usage.d.ts +22 -0
  13. package/lib/agents/skill-usage.js +92 -0
  14. package/lib/agents/wait-for-agents.d.ts +79 -0
  15. package/lib/agents/wait-for-agents.js +256 -0
  16. package/lib/capital-settings.d.ts +12 -1
  17. package/lib/capital-settings.js +23 -3
  18. package/lib/chart/artifact-ref.d.ts +4 -1
  19. package/lib/chart/artifact-ref.js +2 -1
  20. package/lib/chart/delivery.d.ts +15 -1
  21. package/lib/chart/delivery.js +17 -8
  22. package/lib/chart/events.d.ts +1 -1
  23. package/lib/chart/html.js +16 -0
  24. package/lib/chart/present-tool.js +52 -3
  25. package/lib/chart/tool.js +63 -9
  26. package/lib/data-collector/store.d.ts +1 -0
  27. package/lib/data-collector/store.js +2 -2
  28. package/lib/index.d.ts +6 -3
  29. package/lib/index.js +10 -19
  30. package/lib/orchestration-entry.d.ts +11 -0
  31. package/lib/orchestration-entry.js +6 -0
  32. package/lib/present-entry.js +26 -5
  33. package/lib/root-extras-entry.d.ts +2 -5
  34. package/lib/root-extras-entry.js +1 -3
  35. package/lib/selected-skills-entry.d.ts +14 -0
  36. package/lib/selected-skills-entry.js +43 -0
  37. package/lib/web-retriever/local-fetch.d.ts +1 -1
  38. package/lib/web-retriever/local-fetch.js +1 -1
  39. package/lib/web-retriever/wind-client.d.ts +1 -1
  40. package/lib/web-retriever/wind-client.js +1 -1
  41. package/package.json +15 -2
  42. package/preset/capital-generation/README.md +3 -3
  43. package/preset/capital-generation/agent.patch.yml +43 -20
  44. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +3 -2
  45. package/preset/capital-generation/skills/capital-data-query/SKILL.md +32 -0
  46. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +5 -0
  47. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +3 -0
  48. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +8 -10
  49. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +3 -0
  50. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +7 -1
  51. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -0
  52. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +3 -0
  53. package/selected-skills/README.md +5 -4
  54. package/selected-skills/ROUTING.md +88 -0
  55. package/selected-skills/catalog.js +32 -121
  56. package/selected-skills/routing.js +1036 -0
  57. package/watchlist/package.json +1 -1
package/lib/chart/html.js CHANGED
@@ -59,7 +59,9 @@ function formatCapturedAt(epochMs) {
59
59
  export function buildStandaloneHtml(input) {
60
60
  const { payload, library } = input;
61
61
  const meta = payload.meta;
62
+ const kindLabel = payload.kind === 'candlestick' ? 'K 线图' : payload.kind === 'bar' ? 'OHLC 图' : payload.kind === 'column' ? '柱状图' : payload.kind === 'area' ? '面积图' : '折线图';
62
63
  const facts = [];
64
+ facts.push(kindLabel);
63
65
  facts.push(`${meta.points} 个数据点`);
64
66
  if (meta.downsampled)
65
67
  facts.push(`已从 ${meta.original_points} 点下采样(保留极值形状,非全量)`);
@@ -73,6 +75,18 @@ export function buildStandaloneHtml(input) {
73
75
  facts.push(`时间字段 ${meta.time_field}`);
74
76
  if (meta.axis === 'index')
75
77
  facts.push('分类轴(按行序)');
78
+ const guide = [];
79
+ guide.push(meta.axis === 'index' ? '横轴为分类,按数据行序排列;纵轴为数值。' : '横轴为时间;纵轴为数值。');
80
+ if (payload.ohlc)
81
+ guide.push('K 线红涨绿跌,下方成交量柱按当日涨跌着色。');
82
+ else if (payload.volume)
83
+ guide.push('下方成交量柱按涨跌方向着色。');
84
+ if (payload.series.length > 0)
85
+ guide.push(`序列:${payload.series.map((item) => item.label).join('、')}。`);
86
+ if (meta.downsampled)
87
+ guide.push('曲线为下采样后的形状,数值结论请回到表格/查询结果。');
88
+ if (meta.warnings.length > 0)
89
+ guide.push('数据提示见下方 warnings。');
76
90
  const warnings = meta.warnings.length > 0
77
91
  ? `<ul class="warnings">${meta.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>`
78
92
  : '';
@@ -108,6 +122,7 @@ export function buildStandaloneHtml(input) {
108
122
  body { font: 14px/1.6 -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; padding: 16px 20px 24px; }
109
123
  h1 { margin: 0 0 4px; font-size: 16px; font-weight: 600; }
110
124
  .facts { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 12px; }
125
+ .guide { margin: 6px 0 0; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1.7; }
111
126
  main { margin-top: 12px; height: 460px; min-height: 320px; }
112
127
  .warnings { margin: 12px 0 0; padding-left: 18px; color: var(--dsw-alias-label-secondary); font-size: 12px; }
113
128
  footer { margin-top: 16px; color: var(--dsw-alias-label-secondary); font-size: 11px; }
@@ -117,6 +132,7 @@ export function buildStandaloneHtml(input) {
117
132
  <header>
118
133
  <h1>${escapeHtml(payload.title)}</h1>
119
134
  <p class="facts">${facts.map((fact) => escapeHtml(fact)).join(' · ')}</p>
135
+ <p class="guide">${guide.map((line) => escapeHtml(line)).join(' ')}</p>
120
136
  </header>
121
137
  <main id="chart"></main>
122
138
  ${warnings}
@@ -55,6 +55,52 @@ function normalizeTaskId(value) {
55
55
  throw new Error('present_charts 的 task_id 必须是非空字符串');
56
56
  return value.trim();
57
57
  }
58
+ const TITLE_SUBJECT_CODE = /(?<!\d)(\d{6})(?!\d)/g;
59
+ function titleSubjectCodes(title) {
60
+ return [...title.matchAll(TITLE_SUBJECT_CODE)].map((match) => match[1]);
61
+ }
62
+ /** Strip six-digit codes and punctuation so “同花顺 300033 日K” and “同花顺 300803 日K” share a key. */
63
+ function titleDedupeKey(title) {
64
+ return title
65
+ .normalize('NFKC')
66
+ .toLowerCase()
67
+ .replace(TITLE_SUBJECT_CODE, '')
68
+ .replace(/[\s\p{P}\p{S}]+/gu, '');
69
+ }
70
+ function artifactQualityFailure(artifact) {
71
+ const codes = titleSubjectCodes(artifact.title);
72
+ if (codes.length === 0 || artifact.subject_code === null)
73
+ return undefined;
74
+ const mismatched = [...new Set(codes.filter((code) => code !== artifact.subject_code))];
75
+ if (mismatched.length === 0)
76
+ return undefined;
77
+ return `标题里的股票代码 ${mismatched.join('、')} 与 dataset 对应的 ${artifact.subject_code} 不一致;请改正标题后重绘,未登记这张图`;
78
+ }
79
+ function dedupeArtifacts(artifacts) {
80
+ const kept = [];
81
+ const failed = [];
82
+ const seen = new Map();
83
+ for (const artifact of artifacts) {
84
+ const qualityFailure = artifactQualityFailure(artifact);
85
+ if (qualityFailure !== undefined) {
86
+ failed.push({ chart_ref: artifact.chart_ref, reason: qualityFailure });
87
+ continue;
88
+ }
89
+ const titleKey = titleDedupeKey(artifact.title);
90
+ const key = `${artifact.dataset_id ?? 'no-dataset'}|${artifact.kind}|${titleKey.length > 0 ? titleKey : artifact.chart_ref}`;
91
+ const previous = seen.get(key);
92
+ if (previous !== undefined) {
93
+ failed.push({
94
+ chart_ref: artifact.chart_ref,
95
+ reason: `重复图表:与 ${previous.chart_ref} 的 dataset/kind/标题相同;只登记第一张`,
96
+ });
97
+ continue;
98
+ }
99
+ seen.set(key, artifact);
100
+ kept.push(artifact);
101
+ }
102
+ return { kept, failed };
103
+ }
58
104
  /** Present refs first; otherwise present the whole task so compaction cannot strand the charts. */
59
105
  function selectArtifacts(input, session, refs, taskId) {
60
106
  const failed = [];
@@ -89,8 +135,9 @@ export function registerPresentChartsTool(ctx, input) {
89
135
  return;
90
136
  const definition = {
91
137
  name: 'present_charts',
92
- description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented):用户会在答复末尾的「本轮文件改动 / 交付」行看到卡片,点开即渲染自包含 chart.html。'
138
+ description: '把本会话已生成的图表登记为**最终答复的交付物**(官方 deliverables/presented):用户会在答复末尾的「本轮文件改动 / 交付」行看到卡片,点开即渲染自包含 HTML(文件名带代码/标题或图形类型,如 `300803-kline-<短id>.html`)。'
93
139
  + '**由主 Agent 调用一次**:参数是 data_junior 回传的 chart_refs(一次传入,最多 32 个);refs 已丢失时改传 task_id 登记该任务的全部图表。'
140
+ + '登记前会按 dataset + kind + 标题去重,并校验标题里的 6 位股票代码与 dataset 一致;重复或错标题的 ref 会进 failed,不会出现在交付卡片里。'
94
141
  + '这里只记录本轮要交付的图,宿主会在**最终答复之前**统一写入交付行——提前调用不会落进中间轮,不必等“最后一刻”,也不要在过程轮反复调用。'
95
142
  + '不要传文件路径或 HTML(路径由宿主解析);不调用时宿主仍会在收尾时自动登记本 exchange 已生成的图表。',
96
143
  parameters,
@@ -101,7 +148,9 @@ export function registerPresentChartsTool(ctx, input) {
101
148
  if (refs.length === 0 && taskId === undefined) {
102
149
  throw new Error('present_charts 需要 chart_refs 或 task_id:两者都不给就无法确定要登记哪几张图');
103
150
  }
104
- const { selected, failed } = selectArtifacts(input, session, refs, taskId);
151
+ const { selected: selectedRaw, failed } = selectArtifacts(input, session, refs, taskId);
152
+ const { kept: selected, failed: qualityFailed } = dedupeArtifacts(selectedRaw);
153
+ failed.push(...qualityFailed);
105
154
  const receipt = { presented: [], failed };
106
155
  if (selected.length === 0)
107
156
  return receipt;
@@ -124,7 +173,7 @@ export function registerPresentChartsTool(ctx, input) {
124
173
  throw new Error(blocked);
125
174
  const publisher = input.chartEvents();
126
175
  if (publisher === undefined) {
127
- throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开 chart.html');
176
+ throw new Error('图表交付通道不可用(sessions / sessionProjections 解析不到):图表已落盘,可在 workspace 打开 html_path 指向的 HTML');
128
177
  }
129
178
  const accepted = publisher.publishMany !== undefined
130
179
  ? publisher.publishMany(session.id, inputs)
package/lib/chart/tool.js CHANGED
@@ -16,6 +16,49 @@ function newChartId() {
16
16
  const suffix = cryptoLike?.randomUUID ? cryptoLike.randomUUID() : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
17
17
  return `ch_${suffix}`;
18
18
  }
19
+ const SUBJECT_KEYS = ['thscode', 'ts_code', 'ticker', 'symbol', 'stock_code', 'sec_code', 'code'];
20
+ /** Six-digit A-share/HK/US ticker; `thscode` wins over generic API status fields such as `code`. */
21
+ function subjectCodeOf(value) {
22
+ if (!value || typeof value !== 'object' || Array.isArray(value))
23
+ return null;
24
+ const record = value;
25
+ for (const key of SUBJECT_KEYS) {
26
+ const raw = record[key];
27
+ if (typeof raw === 'string') {
28
+ const match = /(?<!\d)(\d{6})(?!\d)/.exec(raw);
29
+ if (match)
30
+ return match[1];
31
+ }
32
+ if (typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 100_000 && raw <= 999_999) {
33
+ return String(raw);
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+ function chartSubjectCode(envelope, rows) {
39
+ const fromEnvelope = subjectCodeOf(envelope);
40
+ if (fromEnvelope !== null)
41
+ return fromEnvelope;
42
+ for (const row of rows.slice(0, 3)) {
43
+ const fromRow = subjectCodeOf(row);
44
+ if (fromRow !== null)
45
+ return fromRow;
46
+ }
47
+ return null;
48
+ }
49
+ /** Readable, collision-resistant filename: ASCII title slug + short chart id. The store's artifact
50
+ * name gate is deliberately ASCII, so Chinese titles fall back to the chart kind (code stays when
51
+ * the title/envelope carries one). */
52
+ function chartHtmlName(title, kind, chartId, subjectCode) {
53
+ const kindSlug = kind === 'candlestick' ? 'kline' : kind === 'bar' ? 'ohlc' : kind === 'column' ? 'column' : kind === 'area' ? 'area' : 'line';
54
+ const words = title.normalize('NFKC').match(/[A-Za-z0-9]+/g) ?? [];
55
+ const parts = subjectCode !== null && !words.includes(subjectCode) ? [subjectCode, ...words] : [...words];
56
+ if (parts.length === 0 || (subjectCode !== null && parts.every((part) => part === subjectCode)))
57
+ parts.push(kindSlug);
58
+ const safe = parts.join('-').replace(/-+/g, '-').replace(/^[.\-]+|[.\-]+$/g, '').slice(0, 48);
59
+ const suffix = chartId.replace(/^ch_/, '').slice(0, 8);
60
+ return `${safe.length > 0 ? safe : kindSlug}-${suffix}.html`;
61
+ }
19
62
  function sessionOf(exec) {
20
63
  const session = exec.agent?.session;
21
64
  if (!session || typeof session.id !== 'string' || session.id.length === 0) {
@@ -125,18 +168,27 @@ export async function renderChart(input, args, exec) {
125
168
  spec.title = args.title.trim().slice(0, 200);
126
169
  // The token carries the direct data-agent session; the nested child never sees raw rows.
127
170
  let rows;
171
+ let subjectCode;
128
172
  let meta;
129
173
  if (datasetId !== undefined) {
130
174
  const readSession = tokenSource?.sourceSession ?? session;
131
- const { ref, rows: datasetRows } = await input.store.readPresentationRows({ session: readSession, dataset_id: datasetId, signal: exec.signal });
175
+ const { ref, rows: datasetRows, envelope } = await input.store.readPresentationRows({ session: readSession, dataset_id: datasetId, signal: exec.signal });
132
176
  rows = datasetRows;
133
- meta = { source_kind: 'dataset', dataset_id: ref.dataset_id, source_label: ref.source_label, captured_at: ref.captured_at };
177
+ subjectCode = chartSubjectCode(envelope, datasetRows);
178
+ meta = {
179
+ source_kind: 'dataset',
180
+ dataset_id: ref.dataset_id,
181
+ source_label: ref.source_label,
182
+ captured_at: ref.captured_at,
183
+ ...(subjectCode === null ? {} : { subject_code: subjectCode }),
184
+ };
134
185
  }
135
186
  else {
136
187
  const parsed = await input.store.readWorkspaceJson({ session, path: path, signal: exec.signal });
137
188
  const extracted = extractRows(parsed);
138
189
  rows = extracted.rows;
139
- meta = { source_kind: 'path' };
190
+ subjectCode = chartSubjectCode(parsed, extracted.rows);
191
+ meta = { source_kind: 'path', ...(subjectCode === null ? {} : { subject_code: subjectCode }) };
140
192
  }
141
193
  const chartId = newChartId();
142
194
  const payload = buildChartPayload({ chart_id: chartId, spec, rows, meta });
@@ -144,13 +196,14 @@ export async function renderChart(input, args, exec) {
144
196
  const generatedAt = now();
145
197
  const html = buildStandaloneHtml({ payload, library, generatedAt });
146
198
  const dir = `${CHART_ARTIFACT_DIR}/${chartId}`;
199
+ const htmlName = chartHtmlName(payload.title, payload.kind, chartId, subjectCode);
147
200
  const written = await input.store.writeWorkspaceFiles({
148
201
  session,
149
202
  dir,
150
203
  files: [
151
- { name: 'spec.json', content: `${JSON.stringify({ chart_id: chartId, created_at: generatedAt, source: { dataset_id: meta.dataset_id ?? null, path: path ?? null }, spec }, null, 2)}\n` },
204
+ { name: 'spec.json', content: `${JSON.stringify({ chart_id: chartId, created_at: generatedAt, source: { dataset_id: meta.dataset_id ?? null, path: path ?? null, subject_code: subjectCode }, spec }, null, 2)}\n` },
152
205
  { name: 'series.json', content: `${JSON.stringify(payload)}\n` },
153
- { name: 'chart.html', content: html },
206
+ { name: htmlName, content: html },
154
207
  ],
155
208
  signal: exec.signal,
156
209
  });
@@ -175,13 +228,14 @@ export async function renderChart(input, args, exec) {
175
228
  dataset_id: meta.dataset_id ?? null,
176
229
  source_label: meta.source_label ?? null,
177
230
  captured_at: meta.captured_at ?? null,
231
+ subject_code: subjectCode,
178
232
  kind: payload.kind,
179
233
  axis: payload.axis,
180
234
  title: payload.title,
181
235
  chart_url: chartUrl,
182
236
  spec_path: pathOf('spec.json'),
183
237
  series_path: pathOf('series.json'),
184
- html_path: pathOf('chart.html'),
238
+ html_path: pathOf(htmlName),
185
239
  created_at: generatedAt,
186
240
  });
187
241
  // Unit callers without the registry still get a stable receipt.
@@ -202,7 +256,7 @@ export async function renderChart(input, args, exec) {
202
256
  ownerSessionId,
203
257
  chart_id: chartId,
204
258
  title: payload.title,
205
- html_path: pathOf('chart.html'),
259
+ html_path: pathOf(htmlName),
206
260
  });
207
261
  deliverableTrace = `publish=${accepted ? '受理' : '拒绝'} ownerSessionId=${ownerSessionId} | ${publisher.lastTrace?.() ?? '(无轨迹)'}`;
208
262
  }
@@ -242,7 +296,7 @@ export async function renderChart(input, args, exec) {
242
296
  chart_url: chartUrl,
243
297
  spec_path: pathOf('spec.json'),
244
298
  series_path: pathOf('series.json'),
245
- html_path: pathOf('chart.html'),
299
+ html_path: pathOf(htmlName),
246
300
  warnings: payload.meta.warnings,
247
301
  };
248
302
  }
@@ -252,7 +306,7 @@ export function registerChartTool(ctx, options) {
252
306
  return;
253
307
  const definition = {
254
308
  name: 'render_chart',
255
- description: `把已经取到的数据渲染成一张可交互图,产物落在 workspace 的 ${CHART_ARTIFACT_DIR}/<chart_id>/(chart.html 可离线打开)。` +
309
+ description: `把已经取到的数据渲染成一张可交互图,产物落在 workspace 的 ${CHART_ARTIFACT_DIR}/<chart_id>/(HTML 以标题/代码命名,如「600519-kline-<短id>.html」,可离线打开)。` +
256
310
  '**只回一条小回执,不回任何原始数据行**:序列走旁路,不进入上下文。' +
257
311
  '来源三选一:dataset_id(已授权的 DatasetRef)、path(workspace 相对路径的 JSON,支持顶层数组或含行数组的 envelope),或 chart_source_ref(宿主签发给 visualization_specialist 的短期 token;使用它时还要提供 task_id)。' +
258
312
  'spec 是自由对象,至少给出 kind(line / area / column / candlestick / bar;后两者要 ohlc 四字段与真实时间列)与字段选择(series 或 ohlc),' +
@@ -353,6 +353,7 @@ export declare class WorkspaceDatasetStore {
353
353
  }): Promise<{
354
354
  ref: DatasetRef;
355
355
  rows: unknown[];
356
+ envelope?: Record<string, unknown>;
356
357
  }>;
357
358
  /**
358
359
  * Host-internal workspace text read; with readWorkspaceBytes it shares containment and size
@@ -332,7 +332,7 @@ export class WorkspaceDatasetStore {
332
332
  if (raw.rows.length > maxRows) {
333
333
  throw new DatasetStoreError('dataset_too_large', `Dataset has ${raw.rows.length} rows; presentation reads are capped at ${maxRows}`);
334
334
  }
335
- return { ref, rows: raw.rows };
335
+ return { ref, rows: raw.rows, ...(raw.envelope === undefined ? {} : { envelope: raw.envelope }) };
336
336
  }
337
337
  /**
338
338
  * Host-internal workspace text read; with readWorkspaceBytes it shares containment and size
@@ -500,7 +500,7 @@ export class WorkspaceDatasetStore {
500
500
  if (isRecord(value)) {
501
501
  const rows = rowsAtPath(value, rowKey ?? 'item');
502
502
  if (rows !== undefined)
503
- return { kind: 'rows', rows, shape: 'envelope_item' };
503
+ return { kind: 'rows', rows, shape: 'envelope_item', envelope: value };
504
504
  }
505
505
  if (isRecord(value))
506
506
  return { kind: 'document', value };
package/lib/index.d.ts CHANGED
@@ -1,15 +1,16 @@
1
1
  import { Context } from '@deepseek-ai/cordis';
2
2
  import z from '@deepseek-ai/schemastery';
3
- import { type SelectedSkillSelection } from './agents/selected-skills-provider.js';
3
+ import { type SelectedSkillSelection, type SelectedSkillsMode } from './agents/selected-skills-provider.js';
4
+ import { CAPITAL_CONFIG_ENTRY_ID } from './capital-settings.js';
4
5
  import { type RetrieverConfig } from './retriever-config.js';
5
6
  export { LOCAL_FETCH_DEFAULTS, PADDLE_OCR_DEFAULTS, resolveLocalFetchConfig, type LocalFetchConfig, type PaddleOcrConfig, type RetrieverConfig, type WindDocsConfig, } from './retriever-config.js';
6
7
  export { resolveUserCustomizationSection } from './user-customization.js';
7
8
  export declare const name = "capital-generation";
8
- /** Entry id shared by `cordis.patch.yml`, `config/index.js` and `client.src.cjs`; drift hides the settings card. */
9
- export declare const CAPITAL_CONFIG_ENTRY_ID = "capital-config";
9
+ export { CAPITAL_CONFIG_ENTRY_ID };
10
10
  export interface Config {
11
11
  customPersona?: string;
12
12
  selectedSkills?: SelectedSkillSelection;
13
+ selectedSkillsMode?: SelectedSkillsMode;
13
14
  fuyaoCredentialRef?: string;
14
15
  ftshareCredentialRef?: string;
15
16
  retriever?: RetrieverConfig;
@@ -21,6 +22,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
21
22
  }>>, Schemastery.ObjectT<NoInfer<{
22
23
  [k: string]: z<boolean, boolean, "defined">;
23
24
  }>>, "defined">;
25
+ selectedSkillsMode: z<"manual" | "auto", "manual" | "auto", "defined">;
24
26
  fuyaoCredentialRef: z<string, string, "defined">;
25
27
  ftshareCredentialRef: z<string, string, "defined">;
26
28
  retriever: z<Schemastery.ObjectS<NoInfer<{
@@ -107,6 +109,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
107
109
  }>>, Schemastery.ObjectT<NoInfer<{
108
110
  [k: string]: z<boolean, boolean, "defined">;
109
111
  }>>, "defined">;
112
+ selectedSkillsMode: z<"manual" | "auto", "manual" | "auto", "defined">;
110
113
  fuyaoCredentialRef: z<string, string, "defined">;
111
114
  ftshareCredentialRef: z<string, string, "defined">;
112
115
  retriever: z<Schemastery.ObjectS<NoInfer<{
package/lib/index.js CHANGED
@@ -10,6 +10,7 @@ import { registerChartTool } from './chart/tool.js';
10
10
  import { createChartEventPublisher } from './chart/events.js';
11
11
  import { chartEventContextOf } from './chart/event-context.js';
12
12
  import { SELECTED_SKILL_CATALOG } from './agents/selected-skills-provider.js';
13
+ import { CAPITAL_CONFIG_ENTRY_ID, readCapitalEntry } from './capital-settings.js';
13
14
  import { registerChartSourceTool } from './chart/source-token.js';
14
15
  import { registerSourceTools, registerWebRetrieverTools } from './web-retriever/tools.js';
15
16
  import { LOCAL_FETCH_DEFAULTS, PADDLE_OCR_DEFAULTS, } from './retriever-config.js';
@@ -17,8 +18,7 @@ import { createRetrieverRuntime } from './retriever-runtime.js';
17
18
  export { LOCAL_FETCH_DEFAULTS, PADDLE_OCR_DEFAULTS, resolveLocalFetchConfig, } from './retriever-config.js';
18
19
  export { resolveUserCustomizationSection } from './user-customization.js';
19
20
  export const name = 'capital-generation';
20
- /** Entry id shared by `cordis.patch.yml`, `config/index.js` and `client.src.cjs`; drift hides the settings card. */
21
- export const CAPITAL_CONFIG_ENTRY_ID = 'capital-config';
21
+ export { CAPITAL_CONFIG_ENTRY_ID };
22
22
  const WindDocsSchema = z.object({
23
23
  endpoint: z.string().default('').description('可选:覆盖 Wind 文档检索端点'),
24
24
  credentialRef: z.string().default('').description('可选:DSH credentials 引用名,空 = WIND_API_KEY'),
@@ -51,13 +51,16 @@ const RetrieverSchema = z.object({
51
51
  // Derive toggles from the catalog: a hand-copied list silently drops defaults at the entry→plugin boundary.
52
52
  const SelectedSkillsSchema = z.object(Object.fromEntries(SELECTED_SKILL_CATALOG.map(({ key, name }) => [
53
53
  key,
54
- z.boolean().default(false).description(`Enable the community experiment "${name}" for the Auto mode root Agent`),
54
+ z.boolean().default(false).description(`Enable the community method "${name}" for the Capital role(s) that own it`),
55
55
  ]))).default(Object.fromEntries(SELECTED_SKILL_CATALOG.map(({ key }) => [key, false])));
56
56
  export const Config = z.object({
57
57
  customPersona: z.string()
58
58
  .default('')
59
59
  .description('自动模式的附加人设文本(独立 section,非 deployment:persona);核心安全约束始终保留'),
60
- selectedSkills: SelectedSkillsSchema.description('Community selected-skill experiments; default off and visible only to the Auto mode root Agent'),
60
+ selectedSkills: SelectedSkillsSchema.description('Community selected methods; default off, each visible only to the Capital role(s) that own it'),
61
+ selectedSkillsMode: z.union(['manual', 'auto'])
62
+ .default('manual')
63
+ .description('精选 Skill 暴露方式:manual = 只用勾选;auto = 按角色开放全部方法,运行期忽略勾选'),
61
64
  fuyaoCredentialRef: z.string()
62
65
  .default('FUYAO_API_KEY')
63
66
  .description('Fuyao credentials 引用名,空 = FUYAO_API_KEY'),
@@ -70,22 +73,10 @@ export const Config = z.object({
70
73
  /** Fallback order: settings.describe → configEditor.entries → boot preset, so unreadable never
71
74
  * reads as "all toggles off". Resolve services per call: boot fibers apply in parallel. */
72
75
  export function readCapitalSettings(ctx, bootConfig) {
73
- const settings = ctx.get('settings');
74
- try {
75
- const resolved = settings?.describe?.().find((entry) => entry.ns === CAPITAL_CONFIG_ENTRY_ID)?.value;
76
+ const entry = readCapitalEntry(ctx);
77
+ if (entry.value !== undefined) {
76
78
  // Re-parse through this plugin's schema: fills defaults and surfaces schema drift.
77
- if (resolved && typeof resolved === 'object')
78
- return { config: Config(resolved), source: 'settings-describe' };
79
- }
80
- catch {
81
- }
82
- const configEditor = ctx.get('configEditor');
83
- try {
84
- const raw = configEditor?.entries?.().find((row) => row?.options?.id === CAPITAL_CONFIG_ENTRY_ID)?.options?.config;
85
- if (raw && typeof raw === 'object')
86
- return { config: Config(raw), source: 'config-editor' };
87
- }
88
- catch {
79
+ return { config: Config(entry.value), source: entry.source ?? 'settings-describe' };
89
80
  }
90
81
  return { config: bootConfig, source: 'preset' };
91
82
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `tools/orchestration` row: the root-only `wait_for_agents` tool.
3
+ *
4
+ * Leaves don't mount this row: a child has no delegated tree to wait for and must use send_message
5
+ * to report upward. The tool also rejects the subagent origin at call time, so a mis-mounted row
6
+ * cannot turn into a child-side polling loop.
7
+ */
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ export declare const name = "capital-orchestration-tools";
10
+ export declare const inject: string[];
11
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,6 @@
1
+ import { registerWaitForAgentsTool } from './agents/wait-for-agents.js';
2
+ export const name = 'capital-orchestration-tools';
3
+ export const inject = ['tools'];
4
+ export function apply(ctx) {
5
+ registerWaitForAgentsTool(ctx);
6
+ }
@@ -2,6 +2,7 @@ import { createChartDelivery } from './chart/delivery.js';
2
2
  import { createChartEventPublisher } from './chart/events.js';
3
3
  import { chartEventContextOf } from './chart/event-context.js';
4
4
  import { registerPresentChartsTool } from './chart/present-tool.js';
5
+ import { createSettledWakeGuard } from './agents/settled-wake.js';
5
6
  import { capitalSharedRuntimeOf } from './chart-source-entry.js';
6
7
  export const name = 'capital-present-tools';
7
8
  export const inject = ['tools'];
@@ -59,10 +60,12 @@ export function apply(ctx) {
59
60
  const shared = capitalSharedRuntimeOf(ctx);
60
61
  const publisherOf = () => createChartEventPublisher(chartEventContextOf(ctx));
61
62
  const eventCtx = ctx;
63
+ const wakeGuard = createSettledWakeGuard();
62
64
  const delivery = createChartDelivery({
63
65
  artifacts: shared.artifacts,
64
66
  publish: (sessionId, inputs) => publisherOf()?.publishMany(sessionId, inputs) ?? false,
65
67
  inboxOf: inboxOf(ctx),
68
+ onClosing: (_session, agent) => wakeGuard.acceptClosing(agent),
66
69
  });
67
70
  // The preset row is applied in the root agent's scope: these events are the root's own.
68
71
  const disposers = [];
@@ -71,10 +74,22 @@ export function apply(ctx) {
71
74
  const payload = args[0];
72
75
  if (payload?.frame === undefined)
73
76
  return;
74
- delivery.acceptStream(payload.agent?.session, payload.frame);
77
+ delivery.acceptStream(payload.agent?.session, payload.frame, payload.agent);
78
+ }));
79
+ disposers.push(eventCtx.on('subagent/start', (...args) => {
80
+ const info = args[0];
81
+ delivery.acceptSubagentStart(info ?? {});
82
+ wakeGuard.acceptSubagentStart(info ?? {});
75
83
  }));
76
- disposers.push(eventCtx.on('subagent/start', (...args) => delivery.acceptSubagentStart(args[0])));
77
84
  disposers.push(eventCtx.on('subagent/end', (...args) => delivery.acceptSubagentEnd(args[0])));
85
+ // Scoped to the root agent: only the root inbox is inspected. A settled notice inserted after
86
+ // the closing frame would otherwise make the loop open a step for a redundant "done" message.
87
+ disposers.push(eventCtx.on('agent/inbox/inserted', (...args) => {
88
+ const payload = args[0];
89
+ if (payload?.agent === undefined || payload.message === undefined)
90
+ return;
91
+ wakeGuard.acceptInboxInserted(payload.agent, payload.message);
92
+ }));
78
93
  }
79
94
  // Session events are not agent-scoped: listen on the unscoped root and filter by payload.
80
95
  const listen = (eventCtx.root ?? ctx);
@@ -92,11 +107,17 @@ export function apply(ctx) {
92
107
  delivery.acceptSubagentEnd({ id: source.senderSessionId });
93
108
  return;
94
109
  }
95
- if (source?.kind !== 'user')
110
+ if (typeof session?.id !== 'string' || source === undefined)
96
111
  return;
97
- if (typeof session?.id !== 'string')
112
+ if (source.kind === 'user') {
113
+ delivery.acceptUserMessage(session.id, typeof event.time === 'number' ? event.time : Date.now());
114
+ wakeGuard.acceptUserMessage(session, source);
98
115
  return;
99
- delivery.acceptUserMessage(session.id, typeof event.time === 'number' ? event.time : Date.now());
116
+ }
117
+ // `user/message` is committed only when admitted, so this is exactly the "child already
118
+ // reported its result" marker the closing guard needs.
119
+ if (source.kind === 'agent-message')
120
+ wakeGuard.acceptUserMessage(session, source);
100
121
  }));
101
122
  }
102
123
  ctx.effect(() => () => {
@@ -1,15 +1,12 @@
1
1
  /**
2
- * `tools/root-extras` row: root-only selected-skills provider and the `customPersona`
3
- * system-prompt section. This row registers no tools; both pieces re-read settings
4
- * every turn so saved changes apply on the next step.
2
+ * `tools/root-extras` row: the `customPersona` system-prompt section. Root-only; this row
3
+ * registers no tools. Selected skills moved to the role-scoped `tools/selected-skills` row.
5
4
  */
6
5
  import type { Context } from '@deepseek-ai/cordis';
7
- import { type SelectedSkillSelection } from './agents/selected-skills-provider.js';
8
6
  export declare const name = "capital-root-extras";
9
7
  export declare const inject: string[];
10
8
  interface RootExtrasConfig {
11
9
  customPersona?: string;
12
- selectedSkills?: SelectedSkillSelection;
13
10
  }
14
11
  /** Re-read per turn so a settings save applies to the same session on the next step. */
15
12
  export declare function readRootExtrasConfig(ctx: Context): RootExtrasConfig;
@@ -1,4 +1,3 @@
1
- import { registerRootSelectedSkills } from './agents/selected-skills-provider.js';
2
1
  import { userCustomizationSection } from './user-customization.js';
3
2
  import { readCapitalConfig } from './capital-settings.js';
4
3
  export const name = 'capital-root-extras';
@@ -6,10 +5,9 @@ export const inject = ['systemPrompt'];
6
5
  /** Re-read per turn so a settings save applies to the same session on the next step. */
7
6
  export function readRootExtrasConfig(ctx) {
8
7
  const config = readCapitalConfig(ctx);
9
- return { customPersona: config.customPersona, selectedSkills: config.selectedSkills };
8
+ return { customPersona: config.customPersona };
10
9
  }
11
10
  export function apply(ctx) {
12
- registerRootSelectedSkills(ctx, () => readRootExtrasConfig(ctx).selectedSkills ?? {});
13
11
  let lastPersonaError;
14
12
  const section = userCustomizationSection(() => readRootExtrasConfig(ctx).customPersona, (error) => {
15
13
  const message = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `tools/selected-skills` row: role-scoped selected-skill catalog plus its role prompt section.
3
+ * The row is mounted once per Capital preset (`config.role`) and registers into that Agent's
4
+ * own scope only; it never writes to the preset standing scope or the host scope.
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ import { type SelectedSkillRole } from './agents/selected-skills-provider.js';
8
+ export declare const name = "capital-selected-skills";
9
+ export declare const inject: string[];
10
+ interface Config {
11
+ role?: SelectedSkillRole;
12
+ }
13
+ export declare function apply(ctx: Context, config?: Config): void;
14
+ export {};
@@ -0,0 +1,43 @@
1
+ import { registerSelectedSkills, renderSelectedSkillsBrief, SELECTED_SKILL_ENTRY_ROLES, } from './agents/selected-skills-provider.js';
2
+ import { readCapitalConfig } from './capital-settings.js';
3
+ export const name = 'capital-selected-skills';
4
+ export const inject = ['systemPrompt'];
5
+ const SECTION_NAME = 'capital:selected-skills';
6
+ /** After the persona, before the user-customization section (order 100). */
7
+ const SECTION_ORDER = 60;
8
+ const accessOf = (ctx) => ({
9
+ selection: () => readCapitalConfig(ctx).selectedSkills ?? {},
10
+ mode: () => (readCapitalConfig(ctx).selectedSkillsMode === 'auto' ? 'auto' : 'manual'),
11
+ });
12
+ export function apply(ctx, config = {}) {
13
+ const role = config.role ?? 'main';
14
+ if (!SELECTED_SKILL_ENTRY_ROLES.includes(role)) {
15
+ throw new Error(`capital-selected-skills: unknown role "${String(role)}"`);
16
+ }
17
+ registerSelectedSkills(ctx, role, accessOf(ctx));
18
+ let lastRenderError;
19
+ ctx.effect(() => {
20
+ const dispose = ctx.systemPrompt.section({
21
+ name: SECTION_NAME,
22
+ order: SECTION_ORDER,
23
+ // Upstream-derived prose can contain `{{...}}`; this section is literal.
24
+ interpolate: false,
25
+ text: (context) => {
26
+ try {
27
+ // dsh-agent passes its Agent as the assembly scope; the usage ledger is keyed by session.
28
+ const agent = context?.agent;
29
+ return renderSelectedSkillsBrief(role, accessOf(ctx), agent);
30
+ }
31
+ catch (error) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ if (message !== lastRenderError) {
34
+ lastRenderError = message;
35
+ ctx.logger?.warn?.(`capital-generation: selected skill prompt unreadable (${message})`);
36
+ }
37
+ return '';
38
+ }
39
+ },
40
+ });
41
+ return typeof dispose === 'function' ? dispose : () => { };
42
+ }, 'capital-generation.selected-skills()');
43
+ }
@@ -3,7 +3,7 @@
3
3
  * Known limit: DNS resolves per request but the IP is not pinned to the connection,
4
4
  * so a rebinding window remains (same trade-off as dsh-search-first).
5
5
  */
6
- export declare const LOCAL_FETCH_CLIENT_VERSION = "2.9.2";
6
+ export declare const LOCAL_FETCH_CLIENT_VERSION = "2.10.1";
7
7
  export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
8
8
  export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
9
9
  export declare class LocalFetchError extends Error {
@@ -4,7 +4,7 @@
4
4
  * so a rebinding window remains (same trade-off as dsh-search-first).
5
5
  */
6
6
  import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
7
- export const LOCAL_FETCH_CLIENT_VERSION = '2.9.2';
7
+ export const LOCAL_FETCH_CLIENT_VERSION = '2.10.1';
8
8
  export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
9
9
  export class LocalFetchError extends Error {
10
10
  code;
@@ -35,7 +35,7 @@ export interface WindCallResult {
35
35
  code?: WindErrorCode;
36
36
  }
37
37
  /** Must match `package.json` version; a test pins the two together. */
38
- export declare const WIND_CLIENT_VERSION = "2.9.2";
38
+ export declare const WIND_CLIENT_VERSION = "2.10.1";
39
39
  export interface WindClient {
40
40
  callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
41
41
  }
@@ -19,7 +19,7 @@ export const WIND_SERVER_ENDPOINTS = {
19
19
  analytics_data: 'https://mcp.wind.com.cn/vserver_analytics_data/mcp/',
20
20
  };
21
21
  /** Must match `package.json` version; a test pins the two together. */
22
- export const WIND_CLIENT_VERSION = '2.9.2';
22
+ export const WIND_CLIENT_VERSION = '2.10.1';
23
23
  function errorText(error) {
24
24
  return error instanceof Error ? error.message : String(error);
25
25
  }