@v587d/capital-generation 2.14.1 → 2.15.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 (39) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +7 -12
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/automation/TABLER-ICONS-LICENSE +21 -0
  5. package/automation/builtins.js +237 -10
  6. package/automation/client.js +37 -5
  7. package/automation/client.src.cjs +578 -126
  8. package/automation/defaults.js +4 -4
  9. package/automation/index.js +1 -0
  10. package/automation/package.json +4 -2
  11. package/automation/route.js +1 -1
  12. package/automation/runner.js +5 -0
  13. package/automation/scheduler.js +34 -29
  14. package/automation/schema.js +39 -5
  15. package/automation/service.js +13 -10
  16. package/automation/store.js +64 -21
  17. package/automation/task-icons.js +75 -0
  18. package/chart-ui/client.js +2 -2
  19. package/chart-ui/package.json +1 -1
  20. package/config/client.js +12 -5
  21. package/config/client.src.cjs +143 -45
  22. package/config/index.js +5 -5
  23. package/config/package.json +1 -1
  24. package/lib/chart/runtime.d.ts +1 -1
  25. package/lib/chart/runtime.js +95 -0
  26. package/lib/chart/series.d.ts +4 -4
  27. package/lib/chart/series.js +44 -10
  28. package/lib/index.js +5 -5
  29. package/lib/web-retriever/local-fetch.d.ts +1 -1
  30. package/lib/web-retriever/local-fetch.js +1 -1
  31. package/lib/web-retriever/sources.d.ts +4 -2
  32. package/lib/web-retriever/sources.js +73 -17
  33. package/lib/web-retriever/wind-client.d.ts +1 -1
  34. package/lib/web-retriever/wind-client.js +1 -1
  35. package/package.json +6 -6
  36. package/preset/capital-generation/README.md +1 -1
  37. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
  38. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
  39. package/watchlist/package.json +1 -1
package/lib/index.js CHANGED
@@ -49,11 +49,11 @@ const RetrieverSchema = z.object({
49
49
  .description('PaddleOCR 文档解析(ocr 工具)配置;Token 走 credentials 引用 PADDLE_OCR_TOKEN'),
50
50
  });
51
51
  const AutomationSchema = z.object({
52
- tickMs: z.number().default(60_000).description('投研自动化调度间隔毫秒(30000–3600000)'),
53
- runTimeoutMinutes: z.number().default(30).description('单次运行 wall-clock 上限分钟(5–120)'),
52
+ tickMs: z.number().default(60_000).description('投研自动化自动检查间隔(毫秒,30000–3600000)'),
53
+ runTimeoutMinutes: z.number().default(60).description('单次运行最长运行时间(分钟,5–120)'),
54
54
  idleTimeoutMinutes: z.number().default(5).description('单次运行空闲超时分钟(1–30)'),
55
- maxSteps: z.number().default(300).description('单次运行最大步骤数(10–2000)'),
56
- maxToolCalls: z.number().default(1_200).description('单次运行最大工具调用数(10–6000)'),
55
+ maxSteps: z.number().default(300).description('单次运行最大步骤数(100–2000)'),
56
+ maxToolCalls: z.number().default(1_200).description('单次运行最大工具调用数(100–5000)'),
57
57
  inAppNotifications: z.boolean().default(true).description('是否写入应用内通知'),
58
58
  });
59
59
  // Derive toggles from the catalog: a hand-copied list silently drops defaults at the entry→plugin boundary.
@@ -77,7 +77,7 @@ export const Config = z.object({
77
77
  .description('FTShare(非凸科技)credentials 引用名,空 = FTSHARE_API_KEY'),
78
78
  automation: AutomationSchema.default({
79
79
  tickMs: 60_000,
80
- runTimeoutMinutes: 30,
80
+ runTimeoutMinutes: 60,
81
81
  idleTimeoutMinutes: 5,
82
82
  maxSteps: 300,
83
83
  maxToolCalls: 1_200,
@@ -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.14.1";
6
+ export declare const LOCAL_FETCH_CLIENT_VERSION = "2.15.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.14.1';
7
+ export const LOCAL_FETCH_CLIENT_VERSION = '2.15.1';
8
8
  export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
9
9
  export class LocalFetchError extends Error {
10
10
  code;
@@ -87,8 +87,10 @@ export declare function resolveSseCompanyUid(transport: Requester, code: string,
87
87
  /** Split Q/A on the reply block class; per-item id markers differ between the two list kinds. */
88
88
  export declare function parseSseFeed(text: string): SourceItem[];
89
89
  /**
90
- * Normalize to Beijing "YYYY-MM-DD HH:MM" (never host local time). All three observed
91
- * forms (X分钟前 / 昨天 HH:MM / absolute) must parse or one row fails the page.
90
+ * Normalize a feed label to Beijing "YYYY-MM-DD HH:MM"; unknown shapes return undefined so
91
+ * the caller fails the page instead of carrying a blank column. The site mixes absolute
92
+ * question times with relative reply times ("昨天 18:22"): every form must parse or one row
93
+ * fails the page. Relative forms use Beijing wall clock, never host local time.
92
94
  */
93
95
  export declare function sseTime(text: string, now?: Date): string | undefined;
94
96
  export declare function sseinfoQa(transport: Requester, input: {
@@ -473,31 +473,87 @@ export function parseSseFeed(text) {
473
473
  }
474
474
  return items;
475
475
  }
476
+ const BEIJING_OFFSET_MS = 8 * 3600 * 1000;
477
+ const ONE_DAY_MS = 86_400_000;
478
+ const SSE_DAY_WORD_DAYS = { 今天: 0, 昨天: 1, 前天: 2 };
479
+ const SSE_ELAPSED_MS = { 秒: 1_000, 分钟: 60_000, 小时: 3_600_000, 天: ONE_DAY_MS };
480
+ /** Beijing wall clock carried in UTC fields, so UTC getters read as Beijing local time. */
481
+ function beijingClock(now) {
482
+ return new Date(now.getTime() + BEIJING_OFFSET_MS);
483
+ }
484
+ function beijingStamp(at) {
485
+ return new Date(at.getTime() + BEIJING_OFFSET_MS).toISOString().slice(0, 16).replace('T', ' ');
486
+ }
487
+ function pad2(value) {
488
+ return String(value).padStart(2, '0');
489
+ }
490
+ /** "HH:MM" from numeric fields; undefined means the label is not a real clock time. */
491
+ function clockText(hour, minute) {
492
+ return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 ? `${pad2(hour)}:${pad2(minute)}` : undefined;
493
+ }
494
+ function realDate(year, month, day) {
495
+ if (month < 1 || month > 12 || day < 1)
496
+ return false;
497
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
498
+ const lengths = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
499
+ return day <= (lengths[month - 1] ?? 0);
500
+ }
476
501
  /**
477
- * Normalize to Beijing "YYYY-MM-DD HH:MM" (never host local time). All three observed
478
- * forms (X分钟前 / 昨天 HH:MM / absolute) must parse or one row fails the page.
502
+ * Normalize a feed label to Beijing "YYYY-MM-DD HH:MM"; unknown shapes return undefined so
503
+ * the caller fails the page instead of carrying a blank column. The site mixes absolute
504
+ * question times with relative reply times ("昨天 18:22"): every form must parse or one row
505
+ * fails the page. Relative forms use Beijing wall clock, never host local time.
479
506
  */
480
507
  export function sseTime(text, now = new Date()) {
481
- const absolute = /(\d{4})年(\d{2})月(\d{2})日\s*(\d{2}:\d{2})/u.exec(text);
508
+ const label = text.trim();
509
+ const absolute = /(\d{4})\s*[年\-/]\s*(\d{1,2})\s*[月\-/]\s*(\d{1,2})\s*日?\s*(\d{1,2}):(\d{2})/u.exec(label);
482
510
  if (absolute !== null) {
483
- return `${absolute[1]}-${absolute[2]}-${absolute[3]} ${absolute[4]}`;
484
- }
485
- const beijing = new Date(now.getTime() + 8 * 3600 * 1000);
486
- const stamp = () => beijing.toISOString().slice(0, 16).replace('T', ' ');
487
- const relative = /(今天|昨天)\s*(\d{2}:\d{2})/u.exec(text);
488
- if (relative !== null) {
489
- if (relative[1] === '昨天')
490
- beijing.setUTCDate(beijing.getUTCDate() - 1);
491
- return `${beijing.toISOString().slice(0, 10)} ${relative[2]}`;
511
+ const [year, month, day] = [Number(absolute[1]), Number(absolute[2]), Number(absolute[3])];
512
+ const time = clockText(Number(absolute[4]), Number(absolute[5]));
513
+ return time === undefined || !realDate(year, month, day)
514
+ ? undefined
515
+ : `${absolute[1]}-${pad2(month)}-${pad2(day)} ${time}`;
516
+ }
517
+ const wall = beijingClock(now);
518
+ const dayWord = /^(今天|昨天|前天)\s*(\d{1,2}):(\d{2})$/u.exec(label);
519
+ if (dayWord !== null) {
520
+ const time = clockText(Number(dayWord[2]), Number(dayWord[3]));
521
+ if (time === undefined)
522
+ return undefined;
523
+ wall.setUTCDate(wall.getUTCDate() - SSE_DAY_WORD_DAYS[dayWord[1]]);
524
+ return `${wall.toISOString().slice(0, 10)} ${time}`;
525
+ }
526
+ // Month-day without a year: more than a day ahead means last year ("12-31" seen in January).
527
+ const monthDay = /^(\d{1,2})-(\d{1,2})\s+(\d{1,2}):(\d{2})$/u.exec(label);
528
+ if (monthDay !== null) {
529
+ const [month, day] = [Number(monthDay[1]), Number(monthDay[2])];
530
+ const time = clockText(Number(monthDay[3]), Number(monthDay[4]));
531
+ if (time === undefined || !realDate(wall.getUTCFullYear(), month, day))
532
+ return undefined;
533
+ let year = wall.getUTCFullYear();
534
+ const candidate = Date.UTC(year, month - 1, day, Number(monthDay[3]), Number(monthDay[4]));
535
+ if (candidate - wall.getTime() > ONE_DAY_MS) {
536
+ if (!realDate(year - 1, month, day))
537
+ return undefined;
538
+ year -= 1;
539
+ }
540
+ return `${String(year)}-${pad2(month)}-${pad2(day)} ${time}`;
492
541
  }
493
- const elapsed = /(\d+)\s*(分钟|小时)前/u.exec(text);
542
+ const elapsed = /^(\d+)\s*(秒|分钟|小时|天)前$/u.exec(label);
494
543
  if (elapsed !== null) {
495
- const amount = Number(elapsed[1]) * (elapsed[2] === '小时' ? 60 : 1);
496
- beijing.setUTCMinutes(beijing.getUTCMinutes() - amount);
497
- return stamp();
544
+ const when = new Date(now.getTime() - Number(elapsed[1]) * SSE_ELAPSED_MS[elapsed[2]]);
545
+ return Number.isNaN(when.getTime()) ? undefined : beijingStamp(when);
498
546
  }
499
547
  // 「刚刚」has no minute precision: use the current minute; otherwise fail (no guessing).
500
- return /^刚刚$/u.test(text.trim()) ? stamp() : undefined;
548
+ if (label === '刚刚' || label === '刚才')
549
+ return beijingStamp(now);
550
+ // Clock only means today in Beijing.
551
+ const clockOnly = /^(\d{1,2}):(\d{2})$/u.exec(label);
552
+ if (clockOnly !== null) {
553
+ const time = clockText(Number(clockOnly[1]), Number(clockOnly[2]));
554
+ return time === undefined ? undefined : `${wall.toISOString().slice(0, 10)} ${time}`;
555
+ }
556
+ return undefined;
501
557
  }
502
558
  export async function sseinfoQa(transport, input) {
503
559
  const type = SSE_KIND[input.kind];
@@ -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.14.1";
38
+ export declare const WIND_CLIENT_VERSION = "2.15.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.14.1';
22
+ export const WIND_CLIENT_VERSION = '2.15.1';
23
23
  function errorText(error) {
24
24
  return error instanceof Error ? error.message : String(error);
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-generation",
3
- "version": "2.14.1",
3
+ "version": "2.15.1",
4
4
  "description": "Capital Generation:面向中国散户的 AI 投资助手插件(自动模式 Agent Preset)",
5
5
  "icon": "assets/cg-icon.png",
6
6
  "author": "v587d",
@@ -162,14 +162,14 @@
162
162
  },
163
163
  "peerDependencies": {
164
164
  "@deepseek-ai/cordis": "^4.0.2",
165
- "@deepseek-ai/dsh-skill": "0.2.1-rc.3",
166
- "@deepseek-ai/dsh-skill-filesystem": "0.2.1-rc.3"
165
+ "@deepseek-ai/dsh-skill": "0.2.1-rc.5",
166
+ "@deepseek-ai/dsh-skill-filesystem": "0.2.1-rc.5"
167
167
  },
168
168
  "devDependencies": {
169
169
  "@deepseek-ai/cordis": "^4.0.2",
170
- "@deepseek-ai/dsh-skill": "^0.2.1-rc.3",
171
- "@deepseek-ai/dsh-skill-filesystem": "^0.2.1-rc.3",
172
- "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.3",
170
+ "@deepseek-ai/dsh-skill": "^0.2.1-rc.5",
171
+ "@deepseek-ai/dsh-skill-filesystem": "^0.2.1-rc.5",
172
+ "@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.5",
173
173
  "@deepseek-ai/schemastery": "^3.18.4",
174
174
  "esbuild": "^0.28.2",
175
175
  "js-yaml": "^5.4.1",
@@ -23,7 +23,7 @@
23
23
 
24
24
  ## 2. 人设(persona)承载与官方语义
25
25
 
26
- 实测 `dsh 0.2.1-rc.3`(`test/persona.test.mjs` 对着本机 dsh 核对字段名),两层载体注册的是
26
+ 实测 `dsh 0.2.1-rc.5`(`test/persona.test.mjs` 对着本机 dsh 核对字段名),两层载体注册的是
27
27
  **同一个 system-prompt 节名**:
28
28
 
29
29
  | 载体 | 位置 | 字段 | 生效范围 |
@@ -83,13 +83,14 @@ description: Use before calling render_chart — the full chart spec vocabulary
83
83
 
84
84
  | 取值 | 解释 |
85
85
  |---|---|
86
- | `2025-01-02` | 业务日;轴上原样显示,不做时区换算 |
87
- | `2025-01-02 08:00:00`(无时区) | 按 **UTC** 解释 |
86
+ | `2025-01-02` | 业务日;全为 date-only 时轴上原样显示;与 epoch 混用时按 **Asia/Shanghai 当日 00:00** 解释 |
87
+ | `2025-01-02 08:00:00`(无时区) | 按 **Asia/Shanghai 墙钟**解释(08:00 = 00:00Z) |
88
88
  | `2025-01-02T08:00:00+08:00` | 按给出偏移解释 |
89
89
  | 数字 / 纯数字字符串,≥ 1e11 | epoch **毫秒** |
90
90
  | 数字 / 纯数字字符串,< 1e11 | epoch **秒** |
91
91
 
92
- 无法解析的行会被丢弃并计入 `warnings.skipped_rows`;时间列**必须**有值。
92
+ 时间轴的 epoch 刻度与十字线统一按 **Asia/Shanghai** 格式化(日 K 显示 `MM-dd`,分钟线显示 `HH:mm`),
93
+ 不会因为库默认按 UTC 而把日期显示早一天。无法解析的行会被丢弃并计入 `warnings.skipped_rows`;时间列**必须**有值。
93
94
  时间轴会被强制升序去重(库的要求),发生重排/去重时 `warnings` 会如实披露。
94
95
 
95
96
  ## 4. 有界化语义(必须会读 warnings)
@@ -99,6 +100,8 @@ description: Use before calling render_chart — the full chart spec vocabulary
99
100
  - **标记点会吸附到最近的保留时间点**,不会因为下采样而消失。
100
101
  - 数值口径与 Dataset 一致:数字,或 Dataset schema 允许的数字字符串;空值/非数值被丢弃并计入 `warnings`。
101
102
  - **分类轴不接受 `range`**:`range` 只用于真实时间轴;分类数据应先在来源侧裁剪,或改用可解析的时间字段。
103
+ - **date-only 的 `range.from/to` 按 Asia/Shanghai 日历日解释,`to` 含当日 23:59:59**;不要把 UTC 零点
104
+ 当成本地日的边界,否则首日会被裁掉。`from/to` 是 datetime 时按给出的时刻精确过滤。
102
105
  - 分类轴 marker 可以用 `index`,也可以把 `time` 写成与 `x` 完全一致的原始分类标签;时间轴 marker 才使用日期/epoch。
103
106
 
104
107
  ## 5. 错误码与一次改对
@@ -54,6 +54,8 @@ failed
54
54
 
55
55
  推荐不是“看到时间字段就必画”。需要同时考虑点数、字段质量、缺失值和用户问题。
56
56
 
57
+ **排名视图的硬前置**:`render_chart` 的 spec **没有 `sort` / `limit`**,类别排名图只能画“来源已经按目标数值排序好”的行。若来源未排序,即使 `recommended` 也要在 gate 记为 `skipped` + `limitation`,用表格承载排名,**不要创建 specialist**——它只能空转数分钟后返回 skipped。确需出图时,先让 collector 产出“已按目标字段排序并裁剪成前 N 行 + 已并入名称”的 Dataset,再重新签发 `chart_source_ref`。
58
+
57
59
  ### 1.3 `blocked`
58
60
 
59
61
  以下情况不得强行出图:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/watchlist",
3
- "version": "2.14.1",
3
+ "version": "2.15.1",
4
4
  "description": "自动模式的用户自选股(宿主级清单 + Fuyao 报价快照 + /capital-watchlist 路由 + 输入框菜单弹窗):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",