dsh-peak-block 0.1.1 → 0.2.0

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/README.md CHANGED
@@ -10,13 +10,27 @@
10
10
 
11
11
  ![拦截效果](docs/拦截效果.webp)
12
12
 
13
+ ## 演示视频
14
+
15
+ | 禁止梁文峰演示 · 24 秒 |
16
+ | :---: |
17
+ | [![禁止梁文峰演示](https://i1.hdslb.com/bfs/archive/2d7698c26fe79cd2438019521a5a099d75ad5d04.jpg)](https://www.bilibili.com/video/BV1zvth6oEFp/) |
18
+
13
19
  ## 安装
14
20
 
21
+ **从 GitHub 安装**:源码在 `src/`,`lib/` 不入仓库,安装时 npm 会触发 `prepare` 脚本现场构建。
22
+
15
23
  ```powershell
16
24
  dsh plugin --profile web add github:better-er/dsh-peak-block
17
25
  ```
18
26
 
19
- 一条命令装完即生效,自动挂载,重启 DSH web 后启用,无需手工编辑任何文件。
27
+ **从 npm 安装**:包内已含构建产物 `lib/index.js`,安装时不再构建。
28
+
29
+ ```powershell
30
+ dsh plugin --profile web add dsh-peak-block
31
+ ```
32
+
33
+ 两种方式装完都会自动挂载,重启 DSH web 后启用,无需手工编辑任何文件。
20
34
 
21
35
  ## 卸载
22
36
 
@@ -59,13 +73,17 @@ plugins:
59
73
  ## 要求与开发
60
74
 
61
75
  - 是**标准形态的 dsh host 单半身插件**:host 在 `agent/request` waterfall 里拦截并切换/阻止,设置全部经 cordis 配置文件注入,见上方配置表,不提供界面配置 UI。
62
- - 无构建:`lib/index.js` 为源码即产物,改完即用。
63
- - 纯函数 `isPeakBeijing` / `isOfficial` / `decide` 在 `lib/index.js` 导出,冒烟测试:
76
+ - TypeScript 源码在 `src/index.ts`,由 tsdown 构建到 `lib/`;`lib/` 是产物不入库,改完要重新构建:
64
77
 
65
78
  ```powershell
66
- node scripts/smoke.mjs
79
+ pnpm install
80
+ pnpm build # 构建 lib/index.js 与 lib/index.d.ts
81
+ pnpm typecheck # 严格类型检查
82
+ pnpm test # vitest 单测,覆盖峰谷判定与拦截决策
67
83
  ```
68
84
 
85
+ - 纯函数 `isPeakBeijing` / `isOfficial` / `decide` 从包入口导出,单测见 `tests/index.spec.ts`。
86
+
69
87
  ## License
70
88
 
71
89
  [MIT](./LICENSE)
package/lib/index.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * dsh-peak-block host 端:梁文峰时间拦截向 DeepSeek 官方 API 的模型请求。
4
+ *
5
+ * 在「梁文峰时间」,即 DeepSeek 官方高峰时段——北京时间工作日 09:00–12:00、14:00–18:00、周末全天谷价——拦截原本发往官方 provider 的对话请求。
6
+ * 已配置 targetProvider 则切换路由,未配置则阻止并抛出带文案的错误提示。非高峰时段不拦截,正常走官方。
7
+ *
8
+ * 拦截 seam:agent/request waterfall。它对每次对话模型请求携带冻结的调用配置种子 LlmCallConfig,含 provider/model/reasoningEffort/temperature/maxTokens/stop。
9
+ * listener 可返回替代 config 切换 provider。要判断请求是否官方必须先调用 next 拿到 config,故实现为「先取 config 再决策」:切换时返回替代 config,阻止时抛带提示文案的错误。
10
+ * compaction 等走 ctx.llm.stream 直接调用的路径不经此 waterfall,本插件不拦。
11
+ */
12
+ /** 插件名,与 cordis.patch.yml 的 name 一致。 */
13
+ declare const name = "dsh-peak-block";
14
+ /** 纯 host 半身,无额外服务注入。 */
15
+ declare const inject: string[];
16
+ /** 峰谷时段窗口。 */
17
+ interface PeakWindow {
18
+ /** JS getUTCDay 序号:0=周日 … 6=周六,1..5 为周一..周五。 */
19
+ days: number[];
20
+ /** 峰时段列表,每项 [起, 止),小时制,含起不含止,按 UTC+8 换算。 */
21
+ hourRanges: Array<[number, number]>;
22
+ /** 周末是否全天谷价,默认 true,即周末不拦。 */
23
+ weekendOffPeak: boolean;
24
+ }
25
+ /** 梁文峰时间默认窗口:工作日 09:00–12:00、14:00–18:00,UTC+8,周末全天谷。 */
26
+ declare const DEFAULT_PEAK: PeakWindow;
27
+ /** 模型请求的冻结调用配置种子,agent/request waterfall 的 next() 返回它,切换 provider 时原样带出其余字段。 */
28
+ interface CallSeed {
29
+ provider?: string;
30
+ model?: string;
31
+ reasoningEffort?: string;
32
+ temperature?: number;
33
+ maxTokens?: number;
34
+ stop?: string[];
35
+ [key: string]: unknown;
36
+ }
37
+ /** 插件配置,全部经 cordis 配置文件注入。 */
38
+ interface Config {
39
+ /** 总开关,显式 false 才关闭,缺省启用。 */
40
+ enabled?: boolean;
41
+ /** 视为「官方」的 provider 精确名单,不设则用默认判定,即只认 deepseek-official。 */
42
+ officialProviders?: string[];
43
+ /** 拦截后切到的目标 provider,留空即阻止并提示。 */
44
+ targetProvider?: string;
45
+ /** 峰谷时段窗口,可整体或局部覆盖默认窗口。 */
46
+ peakWindow?: Partial<PeakWindow>;
47
+ }
48
+ /** 单次请求的决策结果:pass 放行、switch 切 provider、block 阻止。 */
49
+ type Decision = {
50
+ action: 'pass';
51
+ } | {
52
+ action: 'switch';
53
+ config: CallSeed;
54
+ } | {
55
+ action: 'block';
56
+ };
57
+ /** 决策输入,timeMs 由调用方注入以便测试。 */
58
+ interface DecideOptions {
59
+ /** 判定用的 epoch 毫秒,本地时钟不可信时也照此换算 UTC+8。 */
60
+ timeMs: number;
61
+ /** 峰谷窗口,缺省用 DEFAULT_PEAK。 */
62
+ peakWindow?: Partial<PeakWindow>;
63
+ /** 官方 provider 精确名单,缺省只认 deepseek-official。 */
64
+ officialProviders?: string[];
65
+ /** 拦截后切到的目标 provider。 */
66
+ targetProvider?: string;
67
+ }
68
+ /** host 上下文里本插件用到的最小接口,只需注册 agent/request waterfall。 */
69
+ interface HostContext {
70
+ on(event: 'agent/request', listener: (payload: unknown, next: () => Promise<CallSeed>) => Promise<CallSeed>): void;
71
+ }
72
+ /** 默认官方 provider 判定:只精确匹配 DSH 官方注册路由 deepseek-official。
73
+ * 不用名称前缀规则——pi-ai 自带的第三方 deepseek 中转不应算官方,避免误拦。 */
74
+ declare function defaultIsOfficial(provider: string | undefined): boolean;
75
+ /**
76
+ * 时刻是否处于梁文峰时间。纯 UTC+8 数学换算,与系统时区无关,本机时钟/时区不可信。
77
+ * 红线:周末全天谷价,仅工作日有峰。
78
+ */
79
+ declare function isPeakBeijing(timeMs: number, peak?: Partial<PeakWindow>): boolean;
80
+ /** provider 是否视为官方。显式名单用精确匹配;未提供名单用默认判定,即仅 deepseek-official。 */
81
+ declare function isOfficial(provider: string | undefined, officialProviders?: string[]): boolean;
82
+ /** 阻止时抛出的提示文案,用户可见,随界面失败信息呈现。 */
83
+ declare const BLOCK_MESSAGE = "【梁文峰时间拦截 · dsh-peak-block】当前为 DeepSeek 官方高峰时段,且未配置拦截目标 targetProvider,请求已阻止。请为 dsh-peak-block 配置 targetProvider,或将请求留到梁文谷时段再发。";
84
+ /**
85
+ * 纯拦截决策,可单测:返回 { action: 'pass' | 'switch' | 'block', config? }。
86
+ * - 非梁文峰时间或非官方 → pass,放行
87
+ * - 梁文峰时间 + 官方 + 有目标 → switch,切 provider 到目标
88
+ * - 梁文峰时间 + 官方 + 无目标 → block,阻止并提示
89
+ */
90
+ declare function decide(seed: CallSeed | undefined, opts: DecideOptions): Decision;
91
+ declare function apply(ctx: HostContext, config?: Config): void;
92
+ //#endregion
93
+ export { BLOCK_MESSAGE, CallSeed, Config, DEFAULT_PEAK, DecideOptions, Decision, HostContext, PeakWindow, apply, decide, defaultIsOfficial, inject, isOfficial, isPeakBeijing, name };
package/lib/index.js CHANGED
@@ -1,88 +1,93 @@
1
- // dsh-peak-block host 端:梁文峰时间拦截向 DeepSeek 官方 API 的模型请求。
2
- //
3
- // 在「梁文峰时间」,即 DeepSeek 官方高峰时段——北京时间工作日 09:00–12:00、14:00–18:00、周末全天谷价——拦截原本发往官方 provider 的对话请求。
4
- // 已配置 targetProvider 则切换路由,未配置则阻止并抛出带文案的错误提示。非高峰时段不拦截,正常走官方。
5
- //
6
- // 拦截 seam:agent/request waterfall。它对每次对话模型请求携带冻结的调用配置种子 LlmCallConfig,含 provider/model/reasoningEffort/temperature/maxTokens/stop。
7
- // listener 可返回替代 config 切换 provider。要判断请求是否官方必须先调用 next 拿到 config,故实现为「先取 config 再决策」:切换时返回替代 config,阻止时抛带提示文案的错误。
8
- // compaction 等走 ctx.llm.stream 直接调用的路径不经此 waterfall,本插件不拦。
9
-
10
- export const name = 'dsh-peak-block'
11
- export const inject = []
12
-
1
+ //#region src/index.ts
2
+ /**
3
+ * dsh-peak-block host 端:梁文峰时间拦截向 DeepSeek 官方 API 的模型请求。
4
+ *
5
+ * 在「梁文峰时间」,即 DeepSeek 官方高峰时段——北京时间工作日 09:00–12:00、14:00–18:00、周末全天谷价——拦截原本发往官方 provider 的对话请求。
6
+ * 已配置 targetProvider 则切换路由,未配置则阻止并抛出带文案的错误提示。非高峰时段不拦截,正常走官方。
7
+ *
8
+ * 拦截 seam:agent/request waterfall。它对每次对话模型请求携带冻结的调用配置种子 LlmCallConfig,含 provider/model/reasoningEffort/temperature/maxTokens/stop。
9
+ * listener 可返回替代 config 切换 provider。要判断请求是否官方必须先调用 next 拿到 config,故实现为「先取 config 再决策」:切换时返回替代 config,阻止时抛带提示文案的错误。
10
+ * compaction 等走 ctx.llm.stream 直接调用的路径不经此 waterfall,本插件不拦。
11
+ */
12
+ /** 插件名,与 cordis.patch.yml 的 name 一致。 */
13
+ const name = "dsh-peak-block";
14
+ /** 纯 host 半身,无额外服务注入。 */
15
+ const inject = [];
13
16
  /** 梁文峰时间默认窗口:工作日 09:00–12:00、14:00–18:00,UTC+8,周末全天谷。 */
14
- export const DEFAULT_PEAK = {
15
- days: [1, 2, 3, 4, 5], // JS getUTCDay:0=周日 … 6=周六,1..5=周一..周五
16
- hourRanges: [[9, 12], [14, 18]],
17
- weekendOffPeak: true,
18
- }
19
-
20
- /** 默认官方 provider 判定:只精确匹配 DSH 官方注册路由 `deepseek-official`。
21
- * 不用名称前缀规则——pi-ai 自带的第三方 `deepseek` 中转不应算官方,避免误拦。 */
22
- export function defaultIsOfficial(provider) {
23
- return provider === 'deepseek-official'
17
+ const DEFAULT_PEAK = {
18
+ days: [
19
+ 1,
20
+ 2,
21
+ 3,
22
+ 4,
23
+ 5
24
+ ],
25
+ hourRanges: [[9, 12], [14, 18]],
26
+ weekendOffPeak: true
27
+ };
28
+ /** 默认官方 provider 判定:只精确匹配 DSH 官方注册路由 deepseek-official。
29
+ * 不用名称前缀规则——pi-ai 自带的第三方 deepseek 中转不应算官方,避免误拦。 */
30
+ function defaultIsOfficial(provider) {
31
+ return provider === "deepseek-official";
24
32
  }
25
-
26
33
  /**
27
- * 时刻是否处于梁文峰时间。纯 UTC+8 数学换算,与系统时区无关,本机时钟/时区不可信。
28
- * 红线:周末全天谷价,仅工作日有峰。
29
- */
30
- export function isPeakBeijing(timeMs, peak = DEFAULT_PEAK) {
31
- const shifted = timeMs + 8 * 3600 * 1000
32
- const d = new Date(shifted)
33
- const day = d.getUTCDay()
34
- if (peak.weekendOffPeak !== false && (day === 0 || day === 6)) return false
35
- if (Array.isArray(peak.days) && peak.days.length > 0 && !peak.days.includes(day)) return false
36
- const hour = d.getUTCHours()
37
- for (const [a, b] of peak.hourRanges) {
38
- if (hour >= a && hour < b) return true
39
- }
40
- return false
34
+ * 时刻是否处于梁文峰时间。纯 UTC+8 数学换算,与系统时区无关,本机时钟/时区不可信。
35
+ * 红线:周末全天谷价,仅工作日有峰。
36
+ */
37
+ function isPeakBeijing(timeMs, peak = DEFAULT_PEAK) {
38
+ const shifted = timeMs + 288e5;
39
+ const d = new Date(shifted);
40
+ const day = d.getUTCDay();
41
+ if (peak.weekendOffPeak !== false && (day === 0 || day === 6)) return false;
42
+ const days = peak.days;
43
+ if (Array.isArray(days) && days.length > 0 && !days.includes(day)) return false;
44
+ const hour = d.getUTCHours();
45
+ for (const [a, b] of peak.hourRanges ?? DEFAULT_PEAK.hourRanges) if (hour >= a && hour < b) return true;
46
+ return false;
41
47
  }
42
-
43
48
  /** provider 是否视为官方。显式名单用精确匹配;未提供名单用默认判定,即仅 deepseek-official。 */
44
- export function isOfficial(provider, officialProviders) {
45
- if (!provider) return false
46
- if (Array.isArray(officialProviders)) {
47
- return officialProviders.some((p) => provider === p)
48
- }
49
- return defaultIsOfficial(provider)
49
+ function isOfficial(provider, officialProviders) {
50
+ if (!provider) return false;
51
+ if (Array.isArray(officialProviders)) return officialProviders.some((p) => provider === p);
52
+ return defaultIsOfficial(provider);
50
53
  }
51
-
52
54
  /** 阻止时抛出的提示文案,用户可见,随界面失败信息呈现。 */
53
- export const BLOCK_MESSAGE =
54
- '【梁文峰时间拦截 · dsh-peak-block】当前为 DeepSeek 官方高峰时段,且未配置拦截目标 targetProvider,请求已阻止。请为 dsh-peak-block 配置 targetProvider,或将请求留到梁文谷时段再发。'
55
-
55
+ const BLOCK_MESSAGE = "【梁文峰时间拦截 · dsh-peak-block】当前为 DeepSeek 官方高峰时段,且未配置拦截目标 targetProvider,请求已阻止。请为 dsh-peak-block 配置 targetProvider,或将请求留到梁文谷时段再发。";
56
56
  /**
57
- * 纯拦截决策,可单测:返回 { action: 'pass' | 'switch' | 'block', config? }。
58
- * - 非梁文峰时间或非官方 → pass,放行
59
- * - 梁文峰时间 + 官方 + 有目标 → switch,切 provider 到目标
60
- * - 梁文峰时间 + 官方 + 无目标 → block,阻止并提示
61
- */
62
- export function decide(seed, opts) {
63
- if (!isPeakBeijing(opts.timeMs, opts.peakWindow)) return { action: 'pass' }
64
- if (!isOfficial(seed && seed.provider, opts.officialProviders)) return { action: 'pass' }
65
- if (opts.targetProvider) {
66
- return { action: 'switch', config: { ...(seed || {}), provider: opts.targetProvider } }
67
- }
68
- return { action: 'block' }
57
+ * 纯拦截决策,可单测:返回 { action: 'pass' | 'switch' | 'block', config? }。
58
+ * - 非梁文峰时间或非官方 → pass,放行
59
+ * - 梁文峰时间 + 官方 + 有目标 → switch,切 provider 到目标
60
+ * - 梁文峰时间 + 官方 + 无目标 → block,阻止并提示
61
+ */
62
+ function decide(seed, opts) {
63
+ if (!isPeakBeijing(opts.timeMs, opts.peakWindow)) return { action: "pass" };
64
+ if (!isOfficial(seed?.provider, opts.officialProviders)) return { action: "pass" };
65
+ if (opts.targetProvider) return {
66
+ action: "switch",
67
+ config: {
68
+ ...seed ?? {},
69
+ provider: opts.targetProvider
70
+ }
71
+ };
72
+ return { action: "block" };
73
+ }
74
+ function apply(ctx, config = {}) {
75
+ if (config.enabled === false) return;
76
+ const peakWindow = config.peakWindow;
77
+ const officialProviders = config.officialProviders;
78
+ const targetProvider = config.targetProvider;
79
+ ctx.on("agent/request", async (_payload, next) => {
80
+ const seed = await next();
81
+ const decision = decide(seed, {
82
+ timeMs: Date.now(),
83
+ peakWindow,
84
+ officialProviders,
85
+ targetProvider
86
+ });
87
+ if (decision.action === "pass") return seed;
88
+ if (decision.action === "switch") return decision.config;
89
+ throw new Error(BLOCK_MESSAGE);
90
+ });
69
91
  }
70
-
71
- export function apply(ctx, config = {}) {
72
- if (config.enabled === false) return
73
- const peakWindow = config.peakWindow || DEFAULT_PEAK
74
- const officialProviders = config.officialProviders
75
- const targetProvider = config.targetProvider
76
- ctx.on('agent/request', async (_payload, next) => {
77
- const seed = await next()
78
- const decision = decide(seed, {
79
- timeMs: Date.now(),
80
- peakWindow,
81
- officialProviders,
82
- targetProvider,
83
- })
84
- if (decision.action === 'pass') return seed
85
- if (decision.action === 'switch') return decision.config
86
- throw new Error(BLOCK_MESSAGE)
87
- })
88
- }
92
+ //#endregion
93
+ export { BLOCK_MESSAGE, DEFAULT_PEAK, apply, decide, defaultIsOfficial, inject, isOfficial, isPeakBeijing, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-peak-block",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "梁文峰时间拦截官方 DeepSeek API 请求:北京时间工作日高峰时段 09:00-12:00、14:00-18:00 自动拦截发往官方 provider `deepseek-official` 的模型请求,可切换到第三方中转或预留的本机转 API 端口,未配置则阻止并提示;非高峰不拦、正常走官方,专治高峰掉速与刷屏。标准可安装 dsh 插件,host 单半身,设置经 cordis 配置文件注入。",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -17,10 +17,16 @@
17
17
  "type": "git",
18
18
  "url": "git+https://github.com/better-er/dsh-peak-block.git"
19
19
  },
20
+ "author": "betterer",
21
+ "packageManager": "pnpm@9.15.9",
20
22
  "type": "module",
21
23
  "main": "lib/index.js",
24
+ "types": "lib/index.d.ts",
22
25
  "exports": {
23
- ".": "./lib/index.js",
26
+ ".": {
27
+ "types": "./lib/index.d.ts",
28
+ "default": "./lib/index.js"
29
+ },
24
30
  "./cordis.patch.yml": "./cordis.patch.yml",
25
31
  "./package.json": "./package.json"
26
32
  },
@@ -31,8 +37,21 @@
31
37
  },
32
38
  "files": [
33
39
  "lib/index.js",
40
+ "lib/index.d.ts",
34
41
  "cordis.patch.yml",
35
42
  "docs/"
36
43
  ],
37
- "license": "MIT"
44
+ "license": "MIT",
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "prepare": "tsdown",
48
+ "typecheck": "tsc --noEmit --pretty false",
49
+ "test": "vitest run"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.0.0",
53
+ "tsdown": "^0.22.2",
54
+ "typescript": "^5.9.3",
55
+ "vitest": "^4.1.1"
56
+ }
38
57
  }