@dshtrading/api 0.5.0 → 0.6.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.
- package/lib/index.d.ts +123 -21
- package/lib/index.js +4 -1
- package/lib/instrument-form.d.ts +11 -0
- package/lib/instrument-form.js +12 -0
- package/lib/tasks-protocol.d.ts +197 -0
- package/lib/tasks-protocol.js +255 -0
- package/lib/tasks-schedule.d.ts +44 -0
- package/lib/tasks-schedule.js +166 -0
- package/package.json +5 -3
package/lib/index.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
|
+
import { CRON_MAX, DEFAULT_SESSION_PERMISSION, DEFAULT_TASK_AGENT_PRESET, DEFAULT_TASK_PERMISSION, EXECUTION_HISTORY_LIMIT, ExecutionRecord, NewTaskInput, PIN_MAX, PROMPT_MAX, ScheduleRule, TASKS_ACTION_BYTES_LIMIT, TASKS_API_SUBPATH, TASKS_SCHEMA_VERSION, TASK_PERMISSIONS, TITLE_MAX, TaskPermission, TaskRecord, TaskUpdatePatch, TasksAction, TasksActionEnvelope, TasksSchedulerSnapshot, TasksSnapshot, hasOpenExecution, isTaskPermission, parseNewTaskInput, parseTaskUpdatePatch, parseTasksAction, parseTasksEnvelope, permissionPending, permissionRank, retainRecentExecutions } from "./tasks-protocol.js";
|
|
2
|
+
import { CronSchedule, isValidCron, nextRunAtMs, parseCron } from "./tasks-schedule.js";
|
|
3
|
+
import { SWAP_SYMBOL_SUFFIX, instrumentFormOf } from "./instrument-form.js";
|
|
4
|
+
import { Context as CordisContext } from "@deepseek-ai/cordis";
|
|
1
5
|
//#region src/index.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* @dshtrading/api — 纯类型契约包(crypto-slice-plan §关键接口草案)。
|
|
4
|
-
*
|
|
5
|
-
* 零运行时、零依赖:服务契约由连接器实现(ctx 键按市场命名空间,如 ctx.tradingCrypto),
|
|
6
|
-
* 消费方只依赖这里的类型。交易安全闸门(铁律 #3)在类型层面体现为
|
|
7
|
-
* OrderRequest.dryRun 默认 true、错误词汇含 LIVE_TRADING_DISABLED / APPROVAL_DENIED。
|
|
8
|
-
*
|
|
9
|
-
* @module @dshtrading/api
|
|
10
|
-
*/
|
|
11
6
|
/** K线周期(Binance 现货/合约 interval 词汇)。 */
|
|
12
7
|
type Interval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' | '6h' | '8h' | '12h' | '1d' | '3d' | '1w' | '1M';
|
|
13
8
|
/** 最新行情快照(公共数据,无需凭证)。 */
|
|
@@ -422,16 +417,32 @@ interface FundamentalsPackage {
|
|
|
422
417
|
readonly dragonTiger?: DragonTigerItem;
|
|
423
418
|
}
|
|
424
419
|
type PositionSide = 'long' | 'short';
|
|
420
|
+
/**
|
|
421
|
+
* 保证金模式(合约交易语义,P7):`cross` = 全仓、`isolated` = 逐仓。
|
|
422
|
+
* 现货(cash)不使用该字段——缺席即「非保证金/未设置」。
|
|
423
|
+
*/
|
|
424
|
+
type MarginMode = 'cross' | 'isolated';
|
|
425
425
|
/** 持仓快照。 */
|
|
426
426
|
interface Position {
|
|
427
427
|
readonly symbol: string;
|
|
428
428
|
readonly side: PositionSide;
|
|
429
|
-
/** 仓位数量(正数;方向由 side
|
|
429
|
+
/** 仓位数量(正数;方向由 side 表达)。合约仓位是 **base 币数**(连接器已按合约乘数 ctVal 由张换算)。 */
|
|
430
430
|
readonly size: number;
|
|
431
431
|
readonly entryPrice: number;
|
|
432
432
|
readonly markPrice?: number;
|
|
433
433
|
readonly unrealizedPnl?: number;
|
|
434
434
|
readonly leverage?: number;
|
|
435
|
+
/**
|
|
436
|
+
* 强平价(交易所回传原样透传;交易所未提供或不适用时缺席,绝不本地推算强平价——
|
|
437
|
+
* 强平口径与保证金模式、账户级保证金、维持保证金率有关,本地公式会给出错误的安全感)。
|
|
438
|
+
*/
|
|
439
|
+
readonly liquidationPrice?: number;
|
|
440
|
+
/** 维持保证金率(OKX `mgnRatio`;口径由交易所定义,原样透传,不在本地换算或反推)。 */
|
|
441
|
+
readonly marginRatio?: number;
|
|
442
|
+
/** 该仓位的保证金模式;现货持仓与交易所未提供时缺席。 */
|
|
443
|
+
readonly marginMode?: MarginMode;
|
|
444
|
+
/** 名义价值(USD,交易所口径;用于敞口核对,不参与本地风控公式)。 */
|
|
445
|
+
readonly notionalUsd?: number;
|
|
435
446
|
readonly timestamp: number;
|
|
436
447
|
}
|
|
437
448
|
type OrderSide = 'buy' | 'sell';
|
|
@@ -442,10 +453,18 @@ interface OrderRequest {
|
|
|
442
453
|
readonly symbol: string;
|
|
443
454
|
readonly side: OrderSide;
|
|
444
455
|
readonly type: OrderType;
|
|
456
|
+
/** **base 币数量**:合约(永续)由连接器按合约乘数换算成张,消费方恒用币数语义。 */
|
|
445
457
|
readonly quantity: number;
|
|
446
458
|
/** limit 单必填。 */
|
|
447
459
|
readonly price?: number;
|
|
448
|
-
/**
|
|
460
|
+
/**
|
|
461
|
+
* 合约订单的保证金模式(缺省 `cross`)。仅对永续(`-SWAP`)有效:
|
|
462
|
+
* 现货订单只允许 cash 语义,传该字段由连接器结构化拒绝
|
|
463
|
+
* (现货逐仓/杠杆是另一个产品面,本仓未接线)。
|
|
464
|
+
* `isolated` 需先在交易所侧设好该标的的逐仓杠杆(OKX `setLeverage`)。
|
|
465
|
+
*/
|
|
466
|
+
readonly marginMode?: MarginMode;
|
|
467
|
+
/** 缺省/true 时仅模拟,不触碰交易所。实盘还受 @dshtrading/authority 的人工签署授权与 approval 约束 [S4]。 */
|
|
449
468
|
readonly dryRun?: boolean;
|
|
450
469
|
}
|
|
451
470
|
/** 订单回执/状态。 */
|
|
@@ -490,9 +509,49 @@ interface IAccount {
|
|
|
490
509
|
interface Disposable {
|
|
491
510
|
dispose(): void;
|
|
492
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* 标的形态轴(2026-10-08 加密永续与 TradFi 永续落地):与 `market` 正交,**不新增市场键**。
|
|
514
|
+
* 缺省 = `spot`——已存自选、localStorage 与 iOS 冻结契约因此零迁移。
|
|
515
|
+
*
|
|
516
|
+
* 形态由规范符号后缀承载(`-SWAP` = 永续),见 docs/guides/symbol-vocabulary.md;
|
|
517
|
+
* `form` 只出现在名册/检索/wire 元数据里,行情方法不新增 form 形参。
|
|
518
|
+
*/
|
|
519
|
+
type InstrumentForm = 'spot' | 'perp';
|
|
520
|
+
/**
|
|
521
|
+
* 标的资产类别标签(仅展示与检索排序用,不承载交易语义)。
|
|
522
|
+
* **只信交易所元数据**(OKX `instCategory`、Binance `underlyingType`+`underlyingSubType`);
|
|
523
|
+
* 取不到时留空,禁止按符号字面猜(`SPX` 在 OKX 是迷因币 SPX6900,标普 500 是 `US500`)。
|
|
524
|
+
*/
|
|
525
|
+
type InstrumentAssetClass = 'crypto' | 'equity' | 'commodity' | 'index';
|
|
526
|
+
/** 合约专有元数据(仅 `form=perp` 有值):交易所元数据原样透传,不本地推断。 */
|
|
527
|
+
interface InstrumentContract {
|
|
528
|
+
/** 合约乘数(OKX ctVal / Binance contractSize)。 */
|
|
529
|
+
readonly multiplier?: number;
|
|
530
|
+
/** 最小价格变动。 */
|
|
531
|
+
readonly tickSize?: number;
|
|
532
|
+
/** 最小下单量/张数步长。 */
|
|
533
|
+
readonly lotSize?: number;
|
|
534
|
+
/** 交易所允许的最大杠杆。 */
|
|
535
|
+
readonly maxLeverage?: number;
|
|
536
|
+
/** 结算币种(如 USDT)。 */
|
|
537
|
+
readonly settleCcy?: string;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* 标的检索结果的最小元数据(名册/搜索/wire 共用)。
|
|
541
|
+
* 元素形状对既有 `{ symbol, name? }` 实现向后兼容:`form`/`assetClass`/`contract` 全可选。
|
|
542
|
+
*/
|
|
543
|
+
interface InstrumentRef {
|
|
544
|
+
/** 市场规范词汇符号(docs/guides/symbol-vocabulary.md)。 */
|
|
545
|
+
readonly symbol: string;
|
|
546
|
+
readonly name?: string;
|
|
547
|
+
/** 缺省 = spot(老数据零迁移)。 */
|
|
548
|
+
readonly form?: InstrumentForm;
|
|
549
|
+
readonly assetClass?: InstrumentAssetClass;
|
|
550
|
+
readonly contract?: InstrumentContract;
|
|
551
|
+
}
|
|
493
552
|
/**
|
|
494
553
|
* 行情服务契约:由市场连接器实现,注册到按市场命名空间的 ctx 键(如 ctx.tradingCrypto)。
|
|
495
|
-
* 符号词汇(2026-08-31 规范,docs/symbol-vocabulary.md):入参接受市场规范形与连接器原生形,
|
|
554
|
+
* 符号词汇(2026-08-31 规范,docs/guides/symbol-vocabulary.md):入参接受市场规范形与连接器原生形,
|
|
496
555
|
* 输出 `symbol` 一律市场规范形——消费方(GUI/Agent/工作流)与数据源方言解耦。
|
|
497
556
|
*/
|
|
498
557
|
interface MarketDataService {
|
|
@@ -501,13 +560,12 @@ interface MarketDataService {
|
|
|
501
560
|
subscribeTicker(symbol: string, cb: (ticker: Ticker) => void): Disposable;
|
|
502
561
|
/**
|
|
503
562
|
* 查询本市场/交易所支持的全部标的名册(动态全集,Issue #15)。
|
|
504
|
-
* 输出 `symbol` 一律市场规范词汇(docs/symbol-vocabulary.md
|
|
563
|
+
* 输出 `symbol` 一律市场规范词汇(docs/guides/symbol-vocabulary.md),形态由符号后缀承载
|
|
564
|
+
* (`-SWAP` = 永续,`instrumentFormOf` 是唯一裁决实现);每行可带
|
|
565
|
+
* `form`/`assetClass`/`contract` 元数据(InstrumentRef),缺省 = 现货。
|
|
505
566
|
* 可选方法:无公开全集端点的数据源(如 tencent/yahoo/stooq)可缺省或由桥/前端回退。
|
|
506
567
|
*/
|
|
507
|
-
listInstruments?(): Promise<
|
|
508
|
-
symbol: string;
|
|
509
|
-
name?: string;
|
|
510
|
-
}>>;
|
|
568
|
+
listInstruments?(): Promise<InstrumentRef[]>;
|
|
511
569
|
/**
|
|
512
570
|
* 标的基本面与估值快照(GUI「基本面」页签用,2026-09-02)。
|
|
513
571
|
* 可选方法:仅当数据源在同一公共端点里携带基本面字段时实现
|
|
@@ -543,11 +601,41 @@ interface MarketDataService {
|
|
|
543
601
|
getRecentTrades?(symbol: string, limit?: number): Promise<TradeTick[]>;
|
|
544
602
|
}
|
|
545
603
|
/**
|
|
546
|
-
*
|
|
604
|
+
* 杠杆设置请求(合约交易语义,P7;仅 `form=perp` 的标的可传)。
|
|
605
|
+
* dryRun 缺省视为 true:只回本地模拟回执,不触碰交易所(与 placeOrder 同语义)。
|
|
606
|
+
*/
|
|
607
|
+
interface SetLeverageRequest {
|
|
608
|
+
readonly symbol: string;
|
|
609
|
+
/** 目标杠杆倍数(> 0;上限以交易所规格 `InstrumentContract.maxLeverage` 为准,超出即结构化拒绝)。 */
|
|
610
|
+
readonly leverage: number;
|
|
611
|
+
readonly marginMode: MarginMode;
|
|
612
|
+
/** 双向持仓模式下的方向(OKX `posSide`);净持仓模式下缺席。 */
|
|
613
|
+
readonly posSide?: 'long' | 'short';
|
|
614
|
+
/** 全仓模式下按币种设置杠杆时的保证金币(OKX `ccy`);逐仓与净持仓模式缺席。 */
|
|
615
|
+
readonly ccy?: string;
|
|
616
|
+
/** 缺省/true 时仅模拟。实盘还受 @dshtrading/authority 的人工签署授权与 approval 约束 [S4]。 */
|
|
617
|
+
readonly dryRun?: boolean;
|
|
618
|
+
}
|
|
619
|
+
/** 杠杆设置回执(dryRun=true 时是本地模拟回执,不触网)。 */
|
|
620
|
+
interface LeverageSetting {
|
|
621
|
+
readonly symbol: string;
|
|
622
|
+
readonly leverage: number;
|
|
623
|
+
readonly marginMode: MarginMode;
|
|
624
|
+
readonly posSide?: 'long' | 'short';
|
|
625
|
+
readonly ccy?: string;
|
|
626
|
+
/** 本次是否为模拟回执(回执必须显式回带,防 dry-run 语义丢失)。 */
|
|
627
|
+
readonly dryRun: boolean;
|
|
628
|
+
readonly timestamp: number;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* 交易服务契约:placeOrder 默认 dry-run;实盘前必须过 @dshtrading/authority 的人工签署授权与
|
|
547
632
|
* ctx.approval.request(交互形态;headless 下 ask=deny,fail-closed [S4])。
|
|
548
633
|
*
|
|
549
634
|
* R3(okx 切片 2026-08-29)修订:cancelOrder 增加可选 symbol、新增 getOrder——
|
|
550
635
|
* OKX 按 (instId, ordId) 双键定位订单,单参 id 形态不够;无其他实现方,扩展向后兼容。
|
|
636
|
+
*
|
|
637
|
+
* P7(合约交易 Tier 2,2026-10-08)修订:新增可选 `setLeverage`——杠杆/保证金模式变更
|
|
638
|
+
* 是实盘动作,必须与下单同门槛(服务缝三态闸门 + base 审批闸门 + 人工签署授权)。
|
|
551
639
|
*/
|
|
552
640
|
interface TradeService {
|
|
553
641
|
placeOrder(req: OrderRequest): Promise<Order>;
|
|
@@ -572,6 +660,13 @@ interface TradeService {
|
|
|
572
660
|
* 可选方法:时间升序(旧→新),最多 limit 条(缺省 ≤50)。
|
|
573
661
|
*/
|
|
574
662
|
listTradeFills?(symbol?: string, limit?: number): Promise<TradeFill[]>;
|
|
663
|
+
/**
|
|
664
|
+
* 设置合约杠杆与保证金模式(P7 合约交易 Tier 2;仅支持合约的交易所实现)。
|
|
665
|
+
* 可选方法:不实现的连接器缺席即可,消费方按 TRADING_NOT_IMPLEMENTED 语义处理。
|
|
666
|
+
* 这是会改变账户真实风险参数的实盘动作:dryRun 缺省 true(只回本地模拟回执),
|
|
667
|
+
* dryRun=false 必须同时具备人工签署的实盘授权,并经 base 审批闸门。
|
|
668
|
+
*/
|
|
669
|
+
setLeverage?(req: SetLeverageRequest): Promise<LeverageSetting>;
|
|
575
670
|
}
|
|
576
671
|
/**
|
|
577
672
|
* 统一错误词汇(本包不做运行时 Error 类;实现方自行映射到该词汇)。
|
|
@@ -594,8 +689,15 @@ type TradingErrorCode =
|
|
|
594
689
|
* 该码(rest.ts 的 HTTP 非 ok 分支),此前只是漏进了本词汇表(类型层缺口)。
|
|
595
690
|
*/
|
|
596
691
|
'TRADING_UPSTREAM_ERROR' | 'TRADING_INSUFFICIENT_BALANCE' |
|
|
597
|
-
/**
|
|
692
|
+
/** 未获人工签署实盘授权时拒绝实盘(铁律 #3)。 */
|
|
598
693
|
'TRADING_LIVE_TRADING_DISABLED' |
|
|
694
|
+
/**
|
|
695
|
+
* 请求缺一个**必填**的账户定位(当前用点:桥的 `cancel-order` 必须显式 `accId`——
|
|
696
|
+
* 请求里没有 market,HK / US 是两套 trd 上下文,桥不猜)。
|
|
697
|
+
* 与 TRADING_EXCHANGE_ERROR 分开:上游对同类缺格回的是它自己那句错误串,读起来像
|
|
698
|
+
* "venue 拒绝了这笔单",而事实是**我们发错了请求**(客户端手里本来就有全部信息)。
|
|
699
|
+
*/
|
|
700
|
+
'TRADING_ACCOUNT_REQUIRED' |
|
|
599
701
|
/** approval 被拒/无应答(headless fail-closed [S4])。 */
|
|
600
702
|
'TRADING_APPROVAL_DENIED' |
|
|
601
703
|
/** 本次为 dry-run 模拟结果(非故障语义)。 */
|
|
@@ -636,7 +738,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
636
738
|
* 交易服务注册表(issue #40 GUI 交易台,@dshtrading/api 类型声明):
|
|
637
739
|
* 与 tradingMarketDataRegistry 同构的宿主平面注册面——交易连接器 host 面数据行
|
|
638
740
|
* 注册,GUI 桥按路由当前值惰性解析。**注册不改变安全语义**:placeOrder 的
|
|
639
|
-
* 服务缝闸门(dryRun 缺省 true +
|
|
741
|
+
* 服务缝闸门(dryRun 缺省 true + @dshtrading/authority 人工签署授权)随服务实例生效;
|
|
640
742
|
* GUI 桥只放行 dry-run 下单与只读查询,实盘路径仍走 Agent 工具的 base 审批闸门。
|
|
641
743
|
*/
|
|
642
744
|
tradingTradeRegistry: TradeRegistry;
|
|
@@ -837,4 +939,4 @@ interface TradingNewsRegistry {
|
|
|
837
939
|
markets(): string[];
|
|
838
940
|
}
|
|
839
941
|
//#endregion
|
|
840
|
-
export { AccountBalance, AggregateNewsOptions, AggregateNewsResult, AuctionSnapshot, BuybackItem, CompanyProfile, CryptoFundamentals, DerivativesData, DerivativesHistory, DerivativesPoint, Disposable, DividendItem, DragonTigerItem, FinancialCell, FinancialIndicatorRow, FinancialReportGroup, FinancialReportMatrix, FlashFeedService, ForecastSummary, FundamentalsPackage, HolderNumSummary, IAccount, InsiderTradeItem, InstitutionalHoldingItem, Interval, Kline, LimitUpPoolItem, MacroCalendarEntry, MacroFeedService, MacroRateEntry, MainOperationSegment, MarketDataRegistration, MarketDataRegistry, MarketDataService, MarketRouterService, NewsAggregator, NewsItem, OperatingEfficiency, Order, OrderRequest, OrderSide, OrderStatus, OrderType, Orderbook, OrderbookLevel, Position, PositionSide, ResearchReportItem, ShareholderItem, SplitItem, StockFundamentals, Ticker, TradeFill, TradeRegistration, TradeRegistry, TradeService, TradeTick, TradingError, TradingErrorCode, TradingNewsRegistry };
|
|
942
|
+
export { AccountBalance, AggregateNewsOptions, AggregateNewsResult, AuctionSnapshot, BuybackItem, CRON_MAX, CompanyProfile, type CordisContext, CronSchedule, CryptoFundamentals, DEFAULT_SESSION_PERMISSION, DEFAULT_TASK_AGENT_PRESET, DEFAULT_TASK_PERMISSION, DerivativesData, DerivativesHistory, DerivativesPoint, Disposable, DividendItem, DragonTigerItem, EXECUTION_HISTORY_LIMIT, ExecutionRecord, FinancialCell, FinancialIndicatorRow, FinancialReportGroup, FinancialReportMatrix, FlashFeedService, ForecastSummary, FundamentalsPackage, HolderNumSummary, IAccount, InsiderTradeItem, InstitutionalHoldingItem, InstrumentAssetClass, InstrumentContract, InstrumentForm, InstrumentRef, Interval, Kline, LeverageSetting, LimitUpPoolItem, MacroCalendarEntry, MacroFeedService, MacroRateEntry, MainOperationSegment, MarginMode, MarketDataRegistration, MarketDataRegistry, MarketDataService, MarketRouterService, NewTaskInput, NewsAggregator, NewsItem, OperatingEfficiency, Order, OrderRequest, OrderSide, OrderStatus, OrderType, Orderbook, OrderbookLevel, PIN_MAX, PROMPT_MAX, Position, PositionSide, ResearchReportItem, SWAP_SYMBOL_SUFFIX, ScheduleRule, SetLeverageRequest, ShareholderItem, SplitItem, StockFundamentals, TASKS_ACTION_BYTES_LIMIT, TASKS_API_SUBPATH, TASKS_SCHEMA_VERSION, TASK_PERMISSIONS, TITLE_MAX, TaskPermission, TaskRecord, TaskUpdatePatch, TasksAction, TasksActionEnvelope, TasksSchedulerSnapshot, TasksSnapshot, Ticker, TradeFill, TradeRegistration, TradeRegistry, TradeService, TradeTick, TradingError, TradingErrorCode, TradingNewsRegistry, hasOpenExecution, instrumentFormOf, isTaskPermission, isValidCron, nextRunAtMs, parseCron, parseNewTaskInput, parseTaskUpdatePatch, parseTasksAction, parseTasksEnvelope, permissionPending, permissionRank, retainRecentExecutions };
|
package/lib/index.js
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { CRON_MAX, DEFAULT_SESSION_PERMISSION, DEFAULT_TASK_AGENT_PRESET, DEFAULT_TASK_PERMISSION, EXECUTION_HISTORY_LIMIT, PIN_MAX, PROMPT_MAX, TASKS_ACTION_BYTES_LIMIT, TASKS_API_SUBPATH, TASKS_SCHEMA_VERSION, TASK_PERMISSIONS, TITLE_MAX, hasOpenExecution, isTaskPermission, parseNewTaskInput, parseTaskUpdatePatch, parseTasksAction, parseTasksEnvelope, permissionPending, permissionRank, retainRecentExecutions } from "./tasks-protocol.js";
|
|
2
|
+
import { isValidCron, nextRunAtMs, parseCron } from "./tasks-schedule.js";
|
|
3
|
+
import { SWAP_SYMBOL_SUFFIX, instrumentFormOf } from "./instrument-form.js";
|
|
4
|
+
export { CRON_MAX, DEFAULT_SESSION_PERMISSION, DEFAULT_TASK_AGENT_PRESET, DEFAULT_TASK_PERMISSION, EXECUTION_HISTORY_LIMIT, PIN_MAX, PROMPT_MAX, SWAP_SYMBOL_SUFFIX, TASKS_ACTION_BYTES_LIMIT, TASKS_API_SUBPATH, TASKS_SCHEMA_VERSION, TASK_PERMISSIONS, TITLE_MAX, hasOpenExecution, instrumentFormOf, isTaskPermission, isValidCron, nextRunAtMs, parseCron, parseNewTaskInput, parseTaskUpdatePatch, parseTasksAction, parseTasksEnvelope, permissionPending, permissionRank, retainRecentExecutions };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { InstrumentForm } from "./index.js";
|
|
2
|
+
//#region src/instrument-form.d.ts
|
|
3
|
+
/** 永续规范形后缀(规范词汇的唯一载体,见 docs/guides/symbol-vocabulary.md)。 */
|
|
4
|
+
declare const SWAP_SYMBOL_SUFFIX = "-SWAP";
|
|
5
|
+
/**
|
|
6
|
+
* 由规范符号裁决形态:带 `-SWAP` 后缀为 `perp`,否则 `spot`。
|
|
7
|
+
* 输入容忍小写与空白(trim + 大写化)后判定,**不剥后缀**——剥了就不是同一个符号了。
|
|
8
|
+
*/
|
|
9
|
+
declare function instrumentFormOf(symbol: string): InstrumentForm;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { SWAP_SYMBOL_SUFFIX, instrumentFormOf };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/instrument-form.ts
|
|
2
|
+
/** 永续规范形后缀(规范词汇的唯一载体,见 docs/guides/symbol-vocabulary.md)。 */
|
|
3
|
+
const SWAP_SYMBOL_SUFFIX = "-SWAP";
|
|
4
|
+
/**
|
|
5
|
+
* 由规范符号裁决形态:带 `-SWAP` 后缀为 `perp`,否则 `spot`。
|
|
6
|
+
* 输入容忍小写与空白(trim + 大写化)后判定,**不剥后缀**——剥了就不是同一个符号了。
|
|
7
|
+
*/
|
|
8
|
+
function instrumentFormOf(symbol) {
|
|
9
|
+
return symbol.trim().toUpperCase().endsWith("-SWAP") ? "perp" : "spot";
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { SWAP_SYMBOL_SUFFIX, instrumentFormOf };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
//#region src/tasks-protocol.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 右侧栏定时任务——协议层:任务记录形状、动作判别联合与线校验。
|
|
4
|
+
*
|
|
5
|
+
* 设计对齐 dsh-web dsh-task-board 的 Host 权威账本语义(浏览器是异步视图,
|
|
6
|
+
* Host 确认后的 snapshot 才是 UI 状态),但按交易侧栏场景裁剪:
|
|
7
|
+
*
|
|
8
|
+
* - 保留:任务记录(标题/prompt/定时规则/钉住三元组/权限确认门)、有界执行
|
|
9
|
+
* 历史、幂等动作信封、revision 快照、5 段 cron。
|
|
10
|
+
* - 裁剪:看板列状态/描述列/归档/续接冻结卡/交接包/浏览器导入——那些是看板
|
|
11
|
+
* 工作流概念;侧栏定时任务的心智模型是「到点自动开新会话跑 prompt」。
|
|
12
|
+
*
|
|
13
|
+
* 安全面与 task-board 同构:所有变更载荷走严格、版本化的判别联合 + 精确键
|
|
14
|
+
* 校验;浏览器不能写 scheduler 独占时间戳或执行结果;协议不接受命令、可执
|
|
15
|
+
* 行路径、shell 文本或任意参数字段。字符串字段全部有上界。
|
|
16
|
+
*/
|
|
17
|
+
declare const TASKS_SCHEMA_VERSION: 1;
|
|
18
|
+
/** 交易桥上定时任务子路径(挂在 /dshtrading/api 之后)。 */
|
|
19
|
+
declare const TASKS_API_SUBPATH = "/tasks";
|
|
20
|
+
/** 动作载荷上限(对齐 task-board:普通动作 64 KiB)。 */
|
|
21
|
+
declare const TASKS_ACTION_BYTES_LIMIT: number;
|
|
22
|
+
/** 任务可钉住的会话权限预设(/permission <id> 的 id 词汇)。 */
|
|
23
|
+
declare const TASK_PERMISSIONS: readonly ["read-only", "workspace-write", "danger-full-access"];
|
|
24
|
+
/** 单个权限预设 id。 */
|
|
25
|
+
type TaskPermission = (typeof TASK_PERMISSIONS)[number];
|
|
26
|
+
/** 未钉住权限时的会话默认(确认门基准;交易宿主取 workspace-write——日常任务
|
|
27
|
+
* 默认可写,只有 danger-full-access 仍走人工确认门)。 */
|
|
28
|
+
declare const DEFAULT_SESSION_PERMISSION: TaskPermission;
|
|
29
|
+
/** 新建任务未钉住 agent 预设时的插件默认:大师(多 agent 团队主导者)。 */
|
|
30
|
+
declare const DEFAULT_TASK_AGENT_PRESET: "master";
|
|
31
|
+
/**
|
|
32
|
+
* 新建任务未钉住权限时的插件默认。必须有值落盘(钉住):runner 只对钉住权限
|
|
33
|
+
* 执行 /permission——缺省会话回退宿主默认(read-only),拿不到写入。
|
|
34
|
+
*/
|
|
35
|
+
declare const DEFAULT_TASK_PERMISSION: TaskPermission;
|
|
36
|
+
/** 权限等级比较:高于会话默认的钉住权限必须经人工确认才能运行。 */
|
|
37
|
+
declare function permissionRank(permission: TaskPermission): number;
|
|
38
|
+
/** 是否为已知权限预设 id。 */
|
|
39
|
+
declare function isTaskPermission(value: unknown): value is TaskPermission;
|
|
40
|
+
/** 单次真实执行尝试:自己的 id、承载它的 dsh 会话、结算结果。 */
|
|
41
|
+
interface ExecutionRecord {
|
|
42
|
+
/** 执行尝试 id(uuid)。 */
|
|
43
|
+
id: string;
|
|
44
|
+
/** 承载本次执行的 dsh 会话;会话创建落定前缺省。 */
|
|
45
|
+
sessionId: string | undefined;
|
|
46
|
+
/** 启动时刻(ms epoch)。 */
|
|
47
|
+
startedAt: number;
|
|
48
|
+
/** 结算时刻;仍在运行时缺省。 */
|
|
49
|
+
endedAt: number | undefined;
|
|
50
|
+
/** 结算后的结果。 */
|
|
51
|
+
result: 'succeeded' | 'failed' | 'cancelled' | undefined;
|
|
52
|
+
/** 失败时的人类可读原因(prompt 拒绝、agent 出错、启动失败等)。 */
|
|
53
|
+
error: string | undefined;
|
|
54
|
+
/** 本次执行的触发来源。 */
|
|
55
|
+
trigger: 'cron' | 'manual';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 每任务保留的执行历史上限:新执行开始时裁掉最旧的已结算记录,账本体积与
|
|
59
|
+
* 单次写入成本不随历史无限增长。
|
|
60
|
+
*/
|
|
61
|
+
declare const EXECUTION_HISTORY_LIMIT = 20;
|
|
62
|
+
/**
|
|
63
|
+
* 把执行列表裁剪到至多 {@link EXECUTION_HISTORY_LIMIT} 条,最近的在最后。
|
|
64
|
+
* 未结算(运行中)的执行永不裁剪:Host 监视与重启恢复都依赖活跃记录。
|
|
65
|
+
*/
|
|
66
|
+
declare function retainRecentExecutions(executions: readonly ExecutionRecord[]): ExecutionRecord[];
|
|
67
|
+
/** 挂在任务上的定时规则;Host 调度器在 nextRunAt 到期时触发。 */
|
|
68
|
+
interface ScheduleRule {
|
|
69
|
+
/** 是否已启用。 */
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
/** 5 段 cron:分 时 日 月 周(宿主本地时区)。 */
|
|
72
|
+
cron: string;
|
|
73
|
+
/** 下次到期时刻(ms epoch);由调度器/控制器维护。 */
|
|
74
|
+
nextRunAt: number | undefined;
|
|
75
|
+
/** 最近一次定时触发点的时刻(ms epoch)。 */
|
|
76
|
+
lastTriggeredAt: number | undefined;
|
|
77
|
+
}
|
|
78
|
+
/** 一条定时任务。 */
|
|
79
|
+
interface TaskRecord {
|
|
80
|
+
/** 稳定任务 id(uuid)。 */
|
|
81
|
+
id: string;
|
|
82
|
+
/** 短标题(会话重命名同款文案)。 */
|
|
83
|
+
title: string;
|
|
84
|
+
/** 到点发给新 dsh 会话的 prompt。 */
|
|
85
|
+
prompt: string;
|
|
86
|
+
/** 创建时刻(ms epoch)。 */
|
|
87
|
+
createdAt: number;
|
|
88
|
+
/** 最近变更时刻(ms epoch)。 */
|
|
89
|
+
updatedAt: number;
|
|
90
|
+
/** 执行历史,最近在最后(见 {@link retainRecentExecutions})。 */
|
|
91
|
+
executions: ExecutionRecord[];
|
|
92
|
+
/** 定时规则;未排期(仅手动运行)时缺省。 */
|
|
93
|
+
schedule?: ScheduleRule;
|
|
94
|
+
/** 执行必须落在的工作区(workspace id);缺省 = 执行时的最近工作区回退。 */
|
|
95
|
+
workspaceId?: string;
|
|
96
|
+
/** 执行会话使用的 agent 预设(agentPreset id);缺省 = 部署默认。 */
|
|
97
|
+
agentPreset?: string;
|
|
98
|
+
/** 经 /permission <id> 应用到执行会话的权限预设;缺省 = 会话默认。 */
|
|
99
|
+
permission?: TaskPermission;
|
|
100
|
+
/** 高于会话默认权限的人工确认戳(ms epoch);钉住权限/变更即清除(重新武装确认门)。 */
|
|
101
|
+
permissionConfirmedAt?: number;
|
|
102
|
+
}
|
|
103
|
+
/** 新建任务输入(UI → Host)。 */
|
|
104
|
+
interface NewTaskInput {
|
|
105
|
+
title: string;
|
|
106
|
+
prompt: string;
|
|
107
|
+
workspaceId?: string;
|
|
108
|
+
agentPreset?: string;
|
|
109
|
+
permission?: TaskPermission;
|
|
110
|
+
/** 创建时可携带的定时规则;enabled 且 cron 合法才武装。 */
|
|
111
|
+
schedule?: {
|
|
112
|
+
enabled: boolean;
|
|
113
|
+
cron: string;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** 更新补丁:出现的键才更新;显式 null 清除对应钉住字段。 */
|
|
117
|
+
interface TaskUpdatePatch {
|
|
118
|
+
title?: string;
|
|
119
|
+
prompt?: string;
|
|
120
|
+
workspaceId?: string | null;
|
|
121
|
+
agentPreset?: string | null;
|
|
122
|
+
permission?: TaskPermission | null;
|
|
123
|
+
schedule?: {
|
|
124
|
+
enabled: boolean;
|
|
125
|
+
cron: string;
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** 任务是否处于权限确认门待确认状态(定时与手动运行都拒绝)。 */
|
|
129
|
+
declare function permissionPending(task: TaskRecord, sessionDefaultPermission: TaskPermission): boolean;
|
|
130
|
+
/** 任务是否还有未结算的执行(同一任务不并发执行)。 */
|
|
131
|
+
declare function hasOpenExecution(task: TaskRecord): boolean;
|
|
132
|
+
/** 调度器快照(Host 独占字段,浏览器只读)。 */
|
|
133
|
+
interface TasksSchedulerSnapshot {
|
|
134
|
+
timeZone: string;
|
|
135
|
+
lastTickAt?: number;
|
|
136
|
+
error?: string;
|
|
137
|
+
}
|
|
138
|
+
/** 完整 revision 快照:Host 确认后的唯一权威 UI 状态。 */
|
|
139
|
+
interface TasksSnapshot {
|
|
140
|
+
schemaVersion: typeof TASKS_SCHEMA_VERSION;
|
|
141
|
+
revision: number;
|
|
142
|
+
tasks: TaskRecord[];
|
|
143
|
+
scheduler: TasksSchedulerSnapshot;
|
|
144
|
+
/** 确认门比较基准:部署的会话默认权限。 */
|
|
145
|
+
sessionDefaultPermission: TaskPermission;
|
|
146
|
+
}
|
|
147
|
+
type TasksAction = {
|
|
148
|
+
kind: 'create';
|
|
149
|
+
id: string;
|
|
150
|
+
input: NewTaskInput;
|
|
151
|
+
} | {
|
|
152
|
+
kind: 'update';
|
|
153
|
+
taskId: string;
|
|
154
|
+
patch: TaskUpdatePatch;
|
|
155
|
+
} | {
|
|
156
|
+
kind: 'delete';
|
|
157
|
+
taskId: string;
|
|
158
|
+
} | {
|
|
159
|
+
kind: 'set-schedule';
|
|
160
|
+
taskId: string;
|
|
161
|
+
patch: {
|
|
162
|
+
enabled?: boolean;
|
|
163
|
+
cron?: string;
|
|
164
|
+
};
|
|
165
|
+
} | {
|
|
166
|
+
kind: 'run';
|
|
167
|
+
taskId: string;
|
|
168
|
+
} | {
|
|
169
|
+
kind: 'confirm-permission';
|
|
170
|
+
taskId: string;
|
|
171
|
+
};
|
|
172
|
+
/** 动作信封:requestId 供 Host 侧幂等去重(重启后仍有效)。 */
|
|
173
|
+
interface TasksActionEnvelope {
|
|
174
|
+
requestId: string;
|
|
175
|
+
action: TasksAction;
|
|
176
|
+
}
|
|
177
|
+
declare const TITLE_MAX = 200;
|
|
178
|
+
declare const PROMPT_MAX: number;
|
|
179
|
+
declare const PIN_MAX = 200;
|
|
180
|
+
declare const CRON_MAX = 100;
|
|
181
|
+
/** 校验并收敛新建输入;非法返回 undefined(Host 拒绝而不是猜测)。 */
|
|
182
|
+
declare function parseNewTaskInput(value: unknown): NewTaskInput | undefined;
|
|
183
|
+
/**
|
|
184
|
+
* 校验并收敛更新补丁;非法返回 undefined。
|
|
185
|
+
*
|
|
186
|
+
* 显式 null 必须原样保留在补丁里——清除钉住字段的语义由 ledger.applyPatch
|
|
187
|
+
* 落实(patch.workspaceId === null → delete),这里若丢弃 null,UI 编辑器
|
|
188
|
+
* 发出的清除动作会在桥层被静默吞掉(2026-09-04 实证回归,随 agent 工具
|
|
189
|
+
* 面一并修复)。
|
|
190
|
+
*/
|
|
191
|
+
declare function parseTaskUpdatePatch(value: unknown): TaskUpdatePatch | undefined;
|
|
192
|
+
/** 校验动作判别联合;非法返回 undefined。 */
|
|
193
|
+
declare function parseTasksAction(value: unknown): TasksAction | undefined;
|
|
194
|
+
/** 校验动作信封;非法返回 undefined。 */
|
|
195
|
+
declare function parseTasksEnvelope(value: unknown): TasksActionEnvelope | undefined;
|
|
196
|
+
//#endregion
|
|
197
|
+
export { CRON_MAX, DEFAULT_SESSION_PERMISSION, DEFAULT_TASK_AGENT_PRESET, DEFAULT_TASK_PERMISSION, EXECUTION_HISTORY_LIMIT, ExecutionRecord, NewTaskInput, PIN_MAX, PROMPT_MAX, ScheduleRule, TASKS_ACTION_BYTES_LIMIT, TASKS_API_SUBPATH, TASKS_SCHEMA_VERSION, TASK_PERMISSIONS, TITLE_MAX, TaskPermission, TaskRecord, TaskUpdatePatch, TasksAction, TasksActionEnvelope, TasksSchedulerSnapshot, TasksSnapshot, hasOpenExecution, isTaskPermission, parseNewTaskInput, parseTaskUpdatePatch, parseTasksAction, parseTasksEnvelope, permissionPending, permissionRank, retainRecentExecutions };
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
//#region src/tasks-protocol.ts
|
|
2
|
+
/**
|
|
3
|
+
* 右侧栏定时任务——协议层:任务记录形状、动作判别联合与线校验。
|
|
4
|
+
*
|
|
5
|
+
* 设计对齐 dsh-web dsh-task-board 的 Host 权威账本语义(浏览器是异步视图,
|
|
6
|
+
* Host 确认后的 snapshot 才是 UI 状态),但按交易侧栏场景裁剪:
|
|
7
|
+
*
|
|
8
|
+
* - 保留:任务记录(标题/prompt/定时规则/钉住三元组/权限确认门)、有界执行
|
|
9
|
+
* 历史、幂等动作信封、revision 快照、5 段 cron。
|
|
10
|
+
* - 裁剪:看板列状态/描述列/归档/续接冻结卡/交接包/浏览器导入——那些是看板
|
|
11
|
+
* 工作流概念;侧栏定时任务的心智模型是「到点自动开新会话跑 prompt」。
|
|
12
|
+
*
|
|
13
|
+
* 安全面与 task-board 同构:所有变更载荷走严格、版本化的判别联合 + 精确键
|
|
14
|
+
* 校验;浏览器不能写 scheduler 独占时间戳或执行结果;协议不接受命令、可执
|
|
15
|
+
* 行路径、shell 文本或任意参数字段。字符串字段全部有上界。
|
|
16
|
+
*/
|
|
17
|
+
const TASKS_SCHEMA_VERSION = 1;
|
|
18
|
+
/** 交易桥上定时任务子路径(挂在 /dshtrading/api 之后)。 */
|
|
19
|
+
const TASKS_API_SUBPATH = "/tasks";
|
|
20
|
+
/** 动作载荷上限(对齐 task-board:普通动作 64 KiB)。 */
|
|
21
|
+
const TASKS_ACTION_BYTES_LIMIT = 65536;
|
|
22
|
+
/** 任务可钉住的会话权限预设(/permission <id> 的 id 词汇)。 */
|
|
23
|
+
const TASK_PERMISSIONS = [
|
|
24
|
+
"read-only",
|
|
25
|
+
"workspace-write",
|
|
26
|
+
"danger-full-access"
|
|
27
|
+
];
|
|
28
|
+
/** 未钉住权限时的会话默认(确认门基准;交易宿主取 workspace-write——日常任务
|
|
29
|
+
* 默认可写,只有 danger-full-access 仍走人工确认门)。 */
|
|
30
|
+
const DEFAULT_SESSION_PERMISSION = "workspace-write";
|
|
31
|
+
/** 新建任务未钉住 agent 预设时的插件默认:大师(多 agent 团队主导者)。 */
|
|
32
|
+
const DEFAULT_TASK_AGENT_PRESET = "master";
|
|
33
|
+
/**
|
|
34
|
+
* 新建任务未钉住权限时的插件默认。必须有值落盘(钉住):runner 只对钉住权限
|
|
35
|
+
* 执行 /permission——缺省会话回退宿主默认(read-only),拿不到写入。
|
|
36
|
+
*/
|
|
37
|
+
const DEFAULT_TASK_PERMISSION = "workspace-write";
|
|
38
|
+
/** 权限等级比较:高于会话默认的钉住权限必须经人工确认才能运行。 */
|
|
39
|
+
function permissionRank(permission) {
|
|
40
|
+
return TASK_PERMISSIONS.indexOf(permission);
|
|
41
|
+
}
|
|
42
|
+
/** 是否为已知权限预设 id。 */
|
|
43
|
+
function isTaskPermission(value) {
|
|
44
|
+
return typeof value === "string" && TASK_PERMISSIONS.includes(value);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 每任务保留的执行历史上限:新执行开始时裁掉最旧的已结算记录,账本体积与
|
|
48
|
+
* 单次写入成本不随历史无限增长。
|
|
49
|
+
*/
|
|
50
|
+
const EXECUTION_HISTORY_LIMIT = 20;
|
|
51
|
+
/**
|
|
52
|
+
* 把执行列表裁剪到至多 {@link EXECUTION_HISTORY_LIMIT} 条,最近的在最后。
|
|
53
|
+
* 未结算(运行中)的执行永不裁剪:Host 监视与重启恢复都依赖活跃记录。
|
|
54
|
+
*/
|
|
55
|
+
function retainRecentExecutions(executions) {
|
|
56
|
+
if (executions.length <= 20) return [...executions];
|
|
57
|
+
const open = executions.filter((execution) => execution.endedAt === void 0);
|
|
58
|
+
const settled = executions.filter((execution) => execution.endedAt !== void 0);
|
|
59
|
+
const keepSettled = Math.max(20 - open.length, 0);
|
|
60
|
+
return [...settled.slice(Math.max(settled.length - keepSettled, 0)), ...open];
|
|
61
|
+
}
|
|
62
|
+
/** 任务是否处于权限确认门待确认状态(定时与手动运行都拒绝)。 */
|
|
63
|
+
function permissionPending(task, sessionDefaultPermission) {
|
|
64
|
+
return permissionRank(task.permission ?? sessionDefaultPermission) > permissionRank(sessionDefaultPermission) && task.permissionConfirmedAt === void 0;
|
|
65
|
+
}
|
|
66
|
+
/** 任务是否还有未结算的执行(同一任务不并发执行)。 */
|
|
67
|
+
function hasOpenExecution(task) {
|
|
68
|
+
return task.executions.some((execution) => execution.endedAt === void 0);
|
|
69
|
+
}
|
|
70
|
+
const TITLE_MAX = 200;
|
|
71
|
+
const PROMPT_MAX = 65536;
|
|
72
|
+
const PIN_MAX = 200;
|
|
73
|
+
const CRON_MAX = 100;
|
|
74
|
+
function record(value) {
|
|
75
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
76
|
+
}
|
|
77
|
+
function exactKeys(value, allowed) {
|
|
78
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
79
|
+
}
|
|
80
|
+
function boundedString(value, max) {
|
|
81
|
+
return typeof value === "string" && value.length <= max;
|
|
82
|
+
}
|
|
83
|
+
/** undefined 或有界字符串。 */
|
|
84
|
+
function optionalBoundedString(value, max) {
|
|
85
|
+
return value === void 0 || boundedString(value, max);
|
|
86
|
+
}
|
|
87
|
+
/** undefined / null 或有界字符串(补丁语义:null 显式清除钉住字段)。 */
|
|
88
|
+
function optionalBoundedStringOrNull(value, max) {
|
|
89
|
+
return value === void 0 || value === null || boundedString(value, max);
|
|
90
|
+
}
|
|
91
|
+
/** undefined 或「有界且非空白」字符串(标题语义)。 */
|
|
92
|
+
function optionalBoundedNonEmptyString(value, max) {
|
|
93
|
+
return value === void 0 || boundedString(value, max) && value.trim() !== "";
|
|
94
|
+
}
|
|
95
|
+
/** 校验并收敛 schedule 输入;非法返回 undefined。 */
|
|
96
|
+
function scheduleInput(value) {
|
|
97
|
+
const row = record(value);
|
|
98
|
+
if (row === void 0 || !exactKeys(row, ["enabled", "cron"])) return void 0;
|
|
99
|
+
if (typeof row.enabled !== "boolean" || !boundedString(row.cron, 100)) return void 0;
|
|
100
|
+
return {
|
|
101
|
+
enabled: row.enabled,
|
|
102
|
+
cron: row.cron
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** 校验并收敛新建输入;非法返回 undefined(Host 拒绝而不是猜测)。 */
|
|
106
|
+
function parseNewTaskInput(value) {
|
|
107
|
+
const row = record(value);
|
|
108
|
+
if (row === void 0 || !exactKeys(row, [
|
|
109
|
+
"title",
|
|
110
|
+
"prompt",
|
|
111
|
+
"workspaceId",
|
|
112
|
+
"agentPreset",
|
|
113
|
+
"permission",
|
|
114
|
+
"schedule"
|
|
115
|
+
])) return void 0;
|
|
116
|
+
if (!boundedString(row.title, 200) || row.title.trim() === "") return void 0;
|
|
117
|
+
if (!boundedString(row.prompt, 65536)) return void 0;
|
|
118
|
+
const workspaceId = row.workspaceId;
|
|
119
|
+
if (!optionalBoundedString(workspaceId, 200)) return void 0;
|
|
120
|
+
const agentPreset = row.agentPreset;
|
|
121
|
+
if (!optionalBoundedString(agentPreset, 200)) return void 0;
|
|
122
|
+
const permission = row.permission;
|
|
123
|
+
if (permission !== void 0) {
|
|
124
|
+
if (!isTaskPermission(permission)) return void 0;
|
|
125
|
+
}
|
|
126
|
+
const schedule = row.schedule === void 0 ? void 0 : scheduleInput(row.schedule);
|
|
127
|
+
if (row.schedule !== void 0 && schedule === void 0) return void 0;
|
|
128
|
+
return {
|
|
129
|
+
title: row.title,
|
|
130
|
+
prompt: row.prompt,
|
|
131
|
+
...workspaceId === void 0 ? {} : { workspaceId },
|
|
132
|
+
...agentPreset === void 0 ? {} : { agentPreset },
|
|
133
|
+
...permission === void 0 ? {} : { permission },
|
|
134
|
+
...schedule === void 0 ? {} : { schedule }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 校验并收敛更新补丁;非法返回 undefined。
|
|
139
|
+
*
|
|
140
|
+
* 显式 null 必须原样保留在补丁里——清除钉住字段的语义由 ledger.applyPatch
|
|
141
|
+
* 落实(patch.workspaceId === null → delete),这里若丢弃 null,UI 编辑器
|
|
142
|
+
* 发出的清除动作会在桥层被静默吞掉(2026-09-04 实证回归,随 agent 工具
|
|
143
|
+
* 面一并修复)。
|
|
144
|
+
*/
|
|
145
|
+
function parseTaskUpdatePatch(value) {
|
|
146
|
+
const row = record(value);
|
|
147
|
+
if (row === void 0 || !exactKeys(row, [
|
|
148
|
+
"title",
|
|
149
|
+
"prompt",
|
|
150
|
+
"workspaceId",
|
|
151
|
+
"agentPreset",
|
|
152
|
+
"permission",
|
|
153
|
+
"schedule"
|
|
154
|
+
])) return void 0;
|
|
155
|
+
const title = row.title;
|
|
156
|
+
if (title !== void 0 && !optionalBoundedNonEmptyString(title, 200)) return void 0;
|
|
157
|
+
const prompt = row.prompt;
|
|
158
|
+
if (prompt !== void 0 && !optionalBoundedString(prompt, 65536)) return void 0;
|
|
159
|
+
const workspaceId = row.workspaceId;
|
|
160
|
+
if (!optionalBoundedStringOrNull(workspaceId, 200)) return void 0;
|
|
161
|
+
const agentPreset = row.agentPreset;
|
|
162
|
+
if (!optionalBoundedStringOrNull(agentPreset, 200)) return void 0;
|
|
163
|
+
const permission = row.permission;
|
|
164
|
+
if (permission !== void 0 && permission !== null) {
|
|
165
|
+
if (!isTaskPermission(permission)) return void 0;
|
|
166
|
+
}
|
|
167
|
+
const schedule = row.schedule === void 0 ? void 0 : scheduleInput(row.schedule);
|
|
168
|
+
if (row.schedule !== void 0 && schedule === void 0) return void 0;
|
|
169
|
+
return {
|
|
170
|
+
...title === void 0 ? {} : { title },
|
|
171
|
+
...prompt === void 0 ? {} : { prompt },
|
|
172
|
+
...workspaceId === void 0 ? {} : { workspaceId },
|
|
173
|
+
...agentPreset === void 0 ? {} : { agentPreset },
|
|
174
|
+
...permission === void 0 ? {} : { permission },
|
|
175
|
+
...schedule === void 0 ? {} : { schedule }
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/** 校验动作判别联合;非法返回 undefined。 */
|
|
179
|
+
function parseTasksAction(value) {
|
|
180
|
+
const row = record(value);
|
|
181
|
+
if (row === void 0 || typeof row.kind !== "string") return void 0;
|
|
182
|
+
switch (row.kind) {
|
|
183
|
+
case "create": {
|
|
184
|
+
if (!exactKeys(row, [
|
|
185
|
+
"kind",
|
|
186
|
+
"id",
|
|
187
|
+
"input"
|
|
188
|
+
])) return void 0;
|
|
189
|
+
if (!boundedString(row.id, 64) || row.id === "") return void 0;
|
|
190
|
+
const input = parseNewTaskInput(row.input);
|
|
191
|
+
return input === void 0 ? void 0 : {
|
|
192
|
+
kind: "create",
|
|
193
|
+
id: row.id,
|
|
194
|
+
input
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
case "update": {
|
|
198
|
+
if (!exactKeys(row, [
|
|
199
|
+
"kind",
|
|
200
|
+
"taskId",
|
|
201
|
+
"patch"
|
|
202
|
+
])) return void 0;
|
|
203
|
+
if (!boundedString(row.taskId, 64) || row.taskId === "") return void 0;
|
|
204
|
+
const patch = parseTaskUpdatePatch(row.patch);
|
|
205
|
+
return patch === void 0 ? void 0 : {
|
|
206
|
+
kind: "update",
|
|
207
|
+
taskId: row.taskId,
|
|
208
|
+
patch
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
case "delete":
|
|
212
|
+
case "run":
|
|
213
|
+
case "confirm-permission":
|
|
214
|
+
if (!exactKeys(row, ["kind", "taskId"])) return void 0;
|
|
215
|
+
if (!boundedString(row.taskId, 64) || row.taskId === "") return void 0;
|
|
216
|
+
return {
|
|
217
|
+
kind: row.kind,
|
|
218
|
+
taskId: row.taskId
|
|
219
|
+
};
|
|
220
|
+
case "set-schedule": {
|
|
221
|
+
if (!exactKeys(row, [
|
|
222
|
+
"kind",
|
|
223
|
+
"taskId",
|
|
224
|
+
"patch"
|
|
225
|
+
])) return void 0;
|
|
226
|
+
if (!boundedString(row.taskId, 64) || row.taskId === "") return void 0;
|
|
227
|
+
const patch = record(row.patch);
|
|
228
|
+
if (patch === void 0 || !exactKeys(patch, ["enabled", "cron"])) return void 0;
|
|
229
|
+
if (patch.enabled !== void 0 && typeof patch.enabled !== "boolean") return void 0;
|
|
230
|
+
if (patch.cron !== void 0 && !boundedString(patch.cron, 100)) return void 0;
|
|
231
|
+
return {
|
|
232
|
+
kind: "set-schedule",
|
|
233
|
+
taskId: row.taskId,
|
|
234
|
+
patch: {
|
|
235
|
+
...patch.enabled === void 0 ? {} : { enabled: patch.enabled },
|
|
236
|
+
...patch.cron === void 0 ? {} : { cron: patch.cron }
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
default: return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** 校验动作信封;非法返回 undefined。 */
|
|
244
|
+
function parseTasksEnvelope(value) {
|
|
245
|
+
const row = record(value);
|
|
246
|
+
if (row === void 0 || !exactKeys(row, ["requestId", "action"])) return void 0;
|
|
247
|
+
if (!boundedString(row.requestId, 64) || row.requestId === "") return void 0;
|
|
248
|
+
const action = parseTasksAction(row.action);
|
|
249
|
+
return action === void 0 ? void 0 : {
|
|
250
|
+
requestId: row.requestId,
|
|
251
|
+
action
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
export { CRON_MAX, DEFAULT_SESSION_PERMISSION, DEFAULT_TASK_AGENT_PRESET, DEFAULT_TASK_PERMISSION, EXECUTION_HISTORY_LIMIT, PIN_MAX, PROMPT_MAX, TASKS_ACTION_BYTES_LIMIT, TASKS_API_SUBPATH, TASKS_SCHEMA_VERSION, TASK_PERMISSIONS, TITLE_MAX, hasOpenExecution, isTaskPermission, parseNewTaskInput, parseTaskUpdatePatch, parseTasksAction, parseTasksEnvelope, permissionPending, permissionRank, retainRecentExecutions };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/tasks-schedule.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 5 段 cron 解析与下次运行时间计算(右侧栏定时任务的调度纯模块)。
|
|
4
|
+
*
|
|
5
|
+
* 实现移植自 dsh-web 仓库 dsh-task-board 的 core/schedule.ts(2026-09 已在同机
|
|
6
|
+
* 验证过 DST/闰年语义),零依赖、框架无关:调度器(service.ts)与 UI 预览
|
|
7
|
+
* (client 半)共享同一份词汇。语法:五个空白分隔字段 = 分 时 日 月 周;每个
|
|
8
|
+
* 字段支持星号通配、步进(星号/n)、闭区间 a-b、单值与逗号混合列表;周字段 0/7 均为周日;日与周
|
|
9
|
+
* 同时受限时按标准 cron 的 OR 语义合并;时区基准为宿主本地时区。非法表达式
|
|
10
|
+
* parseCron 返回 null,由协议层/UI 拒绝。
|
|
11
|
+
*/
|
|
12
|
+
/** 一条 cron 表达式解析出的各字段匹配集。 */
|
|
13
|
+
interface CronSchedule {
|
|
14
|
+
minutes: ReadonlySet<number>;
|
|
15
|
+
hours: ReadonlySet<number>;
|
|
16
|
+
days: ReadonlySet<number>;
|
|
17
|
+
months: ReadonlySet<number>;
|
|
18
|
+
/** 周几 0-6,0 = 周日(输入 7 归一化为 0)。 */
|
|
19
|
+
weekdays: ReadonlySet<number>;
|
|
20
|
+
/** 日字段是否为字面 '*'(未受限)。 */
|
|
21
|
+
dayWildcard: boolean;
|
|
22
|
+
/** 周字段是否为字面 '*'(未受限)。 */
|
|
23
|
+
weekdayWildcard: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 解析 5 段 cron 表达式。
|
|
27
|
+
* @returns 各字段匹配集;表达式非法时返回 null。
|
|
28
|
+
*/
|
|
29
|
+
declare function parseCron(expr: string): CronSchedule | null;
|
|
30
|
+
/** 表达式是否可解析。 */
|
|
31
|
+
declare function isValidCron(expr: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* 计算 `fromMs`(ms epoch)之后严格晚于它的下一个匹配时刻,本地时区、分钟粒
|
|
34
|
+
* 度。返回匹配分钟的起始 ms epoch;日历上永不可能匹配(如 `0 0 30 2 *`)时
|
|
35
|
+
* 返回 undefined。5 年上限覆盖完整闰年周期,2 月 29 日排期永远可达。
|
|
36
|
+
*
|
|
37
|
+
* 直接从解析出的字段集枚举候选 年/月/日/时/分,而非逐分钟扫描(稀疏表达式如
|
|
38
|
+
* `0 0 29 2 *` 逐分钟要走 ~150 万分钟才到下一个闰日);墙钟字段构造 + 末次
|
|
39
|
+
* matches 复检精确保留旧实现的 DST 语义:春季不存在的分钟向前归一化,秋季重
|
|
40
|
+
* 复小时不会被访问两次。
|
|
41
|
+
*/
|
|
42
|
+
declare function nextRunAtMs(expr: string, fromMs: number): number | undefined;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { CronSchedule, isValidCron, nextRunAtMs, parseCron };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//#region src/tasks-schedule.ts
|
|
2
|
+
/** 各字段的闭区间,按 cron 顺序。 */
|
|
3
|
+
const FIELD_RANGES = [
|
|
4
|
+
[0, 59],
|
|
5
|
+
[0, 23],
|
|
6
|
+
[1, 31],
|
|
7
|
+
[1, 12],
|
|
8
|
+
[0, 7]
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* 解析 5 段 cron 表达式。
|
|
12
|
+
* @returns 各字段匹配集;表达式非法时返回 null。
|
|
13
|
+
*/
|
|
14
|
+
function parseCron(expr) {
|
|
15
|
+
const fields = expr.trim().split(/\s+/);
|
|
16
|
+
if (fields.length !== 5) return null;
|
|
17
|
+
const sets = [];
|
|
18
|
+
for (let index = 0; index < 5; index++) {
|
|
19
|
+
const bounds = FIELD_RANGES[index];
|
|
20
|
+
const field = fields[index];
|
|
21
|
+
const set = /* @__PURE__ */ new Set();
|
|
22
|
+
if (bounds === void 0 || field === void 0 || !parseField(field, bounds[0], bounds[1], set)) return null;
|
|
23
|
+
sets.push(set);
|
|
24
|
+
}
|
|
25
|
+
const weekdays = /* @__PURE__ */ new Set();
|
|
26
|
+
for (const day of sets[4]) weekdays.add(day === 7 ? 0 : day);
|
|
27
|
+
return {
|
|
28
|
+
minutes: sets[0],
|
|
29
|
+
hours: sets[1],
|
|
30
|
+
days: sets[2],
|
|
31
|
+
months: sets[3],
|
|
32
|
+
weekdays,
|
|
33
|
+
dayWildcard: fields[2] === "*",
|
|
34
|
+
weekdayWildcard: fields[4] === "*"
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** 表达式是否可解析。 */
|
|
38
|
+
function isValidCron(expr) {
|
|
39
|
+
return parseCron(expr) !== null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 计算 `fromMs`(ms epoch)之后严格晚于它的下一个匹配时刻,本地时区、分钟粒
|
|
43
|
+
* 度。返回匹配分钟的起始 ms epoch;日历上永不可能匹配(如 `0 0 30 2 *`)时
|
|
44
|
+
* 返回 undefined。5 年上限覆盖完整闰年周期,2 月 29 日排期永远可达。
|
|
45
|
+
*
|
|
46
|
+
* 直接从解析出的字段集枚举候选 年/月/日/时/分,而非逐分钟扫描(稀疏表达式如
|
|
47
|
+
* `0 0 29 2 *` 逐分钟要走 ~150 万分钟才到下一个闰日);墙钟字段构造 + 末次
|
|
48
|
+
* matches 复检精确保留旧实现的 DST 语义:春季不存在的分钟向前归一化,秋季重
|
|
49
|
+
* 复小时不会被访问两次。
|
|
50
|
+
*/
|
|
51
|
+
function nextRunAtMs(expr, fromMs) {
|
|
52
|
+
const schedule = parseCron(expr);
|
|
53
|
+
if (schedule === null) return void 0;
|
|
54
|
+
if (!hasPossibleCalendarDay(schedule)) return void 0;
|
|
55
|
+
const from = new Date(fromMs);
|
|
56
|
+
const limitMs = fromMs + 158112e6;
|
|
57
|
+
const sortedMinutes = [...schedule.minutes].sort((a, b) => a - b);
|
|
58
|
+
const sortedHours = [...schedule.hours].sort((a, b) => a - b);
|
|
59
|
+
const sortedMonths = [...schedule.months].sort((a, b) => a - b);
|
|
60
|
+
let year = from.getFullYear();
|
|
61
|
+
let month = from.getMonth() + 1;
|
|
62
|
+
let day = from.getDate();
|
|
63
|
+
let hour = from.getHours();
|
|
64
|
+
let minute = from.getMinutes() + 1;
|
|
65
|
+
while (new Date(year, month - 1, 1, 0, 0, 0, 0).getTime() <= limitMs) {
|
|
66
|
+
for (const candidateMonth of sortedMonths) {
|
|
67
|
+
if (candidateMonth < month) continue;
|
|
68
|
+
const daysInMonth = new Date(year, candidateMonth, 0).getDate();
|
|
69
|
+
const dayStart = candidateMonth === month ? day : 1;
|
|
70
|
+
for (let candidateDay = dayStart; candidateDay <= daysInMonth; candidateDay += 1) {
|
|
71
|
+
if (!dayCandidate(schedule, new Date(year, candidateMonth - 1, candidateDay, 0, 0, 0, 0))) continue;
|
|
72
|
+
const hourStart = candidateMonth === month && candidateDay === day ? hour : 0;
|
|
73
|
+
for (const candidateHour of sortedHours) {
|
|
74
|
+
if (candidateHour < hourStart) continue;
|
|
75
|
+
const minuteStart = candidateMonth === month && candidateDay === day && candidateHour === hour ? minute : 0;
|
|
76
|
+
for (const candidateMinute of sortedMinutes) {
|
|
77
|
+
if (candidateMinute < minuteStart) continue;
|
|
78
|
+
const candidate = new Date(year, candidateMonth - 1, candidateDay, candidateHour, candidateMinute, 0, 0);
|
|
79
|
+
const time = candidate.getTime();
|
|
80
|
+
if (time <= fromMs) continue;
|
|
81
|
+
if (time > limitMs) return void 0;
|
|
82
|
+
if (matches(schedule, candidate)) return time;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
year += 1;
|
|
88
|
+
month = 1;
|
|
89
|
+
day = 1;
|
|
90
|
+
hour = 0;
|
|
91
|
+
minute = 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** 日/周 OR 闸门:matches 与候选扫描共用。 */
|
|
95
|
+
function dayCandidate(schedule, date) {
|
|
96
|
+
const dayMatches = schedule.days.has(date.getDate());
|
|
97
|
+
const weekdayMatches = schedule.weekdays.has(date.getDay());
|
|
98
|
+
if (schedule.dayWildcard) return weekdayMatches;
|
|
99
|
+
if (schedule.weekdayWildcard) return dayMatches;
|
|
100
|
+
return dayMatches || weekdayMatches;
|
|
101
|
+
}
|
|
102
|
+
/** 提前否决日历上不可能的 月/日 组合,省掉多年扫描。 */
|
|
103
|
+
function hasPossibleCalendarDay(schedule) {
|
|
104
|
+
if (schedule.dayWildcard || !schedule.weekdayWildcard) return true;
|
|
105
|
+
const maximumDay = /* @__PURE__ */ new Map([
|
|
106
|
+
[1, 31],
|
|
107
|
+
[2, 29],
|
|
108
|
+
[3, 31],
|
|
109
|
+
[4, 30],
|
|
110
|
+
[5, 31],
|
|
111
|
+
[6, 30],
|
|
112
|
+
[7, 31],
|
|
113
|
+
[8, 31],
|
|
114
|
+
[9, 30],
|
|
115
|
+
[10, 31],
|
|
116
|
+
[11, 30],
|
|
117
|
+
[12, 31]
|
|
118
|
+
]);
|
|
119
|
+
for (const month of schedule.months) {
|
|
120
|
+
const maximum = maximumDay.get(month) ?? 0;
|
|
121
|
+
if ([...schedule.days].some((day) => day <= maximum)) return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
/** 解析单个逗号列表字段到匹配集。 */
|
|
126
|
+
function parseField(field, min, max, out) {
|
|
127
|
+
if (field === "*") {
|
|
128
|
+
for (let value = min; value <= max; value++) out.add(value);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
for (const part of field.split(",")) {
|
|
132
|
+
if (part === "") return false;
|
|
133
|
+
const [range = "", stepRaw] = part.split("/");
|
|
134
|
+
let low;
|
|
135
|
+
let high;
|
|
136
|
+
if (range === "*") {
|
|
137
|
+
low = min;
|
|
138
|
+
high = max;
|
|
139
|
+
} else if (range.includes("-")) {
|
|
140
|
+
const [a = "", b = ""] = range.split("-");
|
|
141
|
+
if (a === "" || b === "" || !isDigits(a) || !isDigits(b)) return false;
|
|
142
|
+
low = Number(a);
|
|
143
|
+
high = Number(b);
|
|
144
|
+
} else if (isDigits(range)) {
|
|
145
|
+
low = Number(range);
|
|
146
|
+
high = Number(range);
|
|
147
|
+
} else return false;
|
|
148
|
+
if (low < min || high > max || low > high) return false;
|
|
149
|
+
const step = stepRaw === void 0 ? 1 : isDigits(stepRaw) ? Number(stepRaw) : NaN;
|
|
150
|
+
if (!Number.isInteger(step) || step < 1) return false;
|
|
151
|
+
for (let value = low; value <= high; value += step) out.add(value);
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
/** 日/周 OR 语义:某一面受限即按该面闸门。 */
|
|
156
|
+
function matches(schedule, date) {
|
|
157
|
+
if (!schedule.minutes.has(date.getMinutes())) return false;
|
|
158
|
+
if (!schedule.hours.has(date.getHours())) return false;
|
|
159
|
+
if (!schedule.months.has(date.getMonth() + 1)) return false;
|
|
160
|
+
return dayCandidate(schedule, date);
|
|
161
|
+
}
|
|
162
|
+
function isDigits(value) {
|
|
163
|
+
return /^\d+$/.test(value);
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { isValidCron, nextRunAtMs, parseCron };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/api",
|
|
3
3
|
"description": "Pure TypeScript contracts for dsh-trading: market-data/trade service interfaces, domain types, and the shared error vocabulary",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
21
|
-
"tsdown": "^0.22.0"
|
|
21
|
+
"tsdown": "^0.22.0",
|
|
22
|
+
"vitest": "^3.0.0"
|
|
22
23
|
},
|
|
23
24
|
"scripts": {
|
|
24
|
-
"build": "tsdown"
|
|
25
|
+
"build": "tsdown",
|
|
26
|
+
"test": "vitest run"
|
|
25
27
|
}
|
|
26
28
|
}
|