dsh-kenari-plugin 0.1.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/LICENSE +21 -0
- package/README.en.md +281 -0
- package/README.md +283 -0
- package/assets/kenari-favicon-128.png +0 -0
- package/assets/kenari-favicon-256.png +0 -0
- package/client/index.js +1998 -0
- package/cordis.patch.yml +137 -0
- package/lib/billing.d.ts +120 -0
- package/lib/billing.d.ts.map +1 -0
- package/lib/billing.js +209 -0
- package/lib/billing.js.map +1 -0
- package/lib/catalog-view.d.ts +78 -0
- package/lib/catalog-view.d.ts.map +1 -0
- package/lib/catalog-view.js +113 -0
- package/lib/catalog-view.js.map +1 -0
- package/lib/catalog.d.ts +239 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +445 -0
- package/lib/catalog.js.map +1 -0
- package/lib/default-route.d.ts +35 -0
- package/lib/default-route.d.ts.map +1 -0
- package/lib/default-route.js +111 -0
- package/lib/default-route.js.map +1 -0
- package/lib/errors.d.ts +29 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +74 -0
- package/lib/errors.js.map +1 -0
- package/lib/favicon.d.ts +24 -0
- package/lib/favicon.d.ts.map +1 -0
- package/lib/favicon.js +68 -0
- package/lib/favicon.js.map +1 -0
- package/lib/http.d.ts +104 -0
- package/lib/http.d.ts.map +1 -0
- package/lib/http.js +247 -0
- package/lib/http.js.map +1 -0
- package/lib/index.d.ts +83 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +285 -0
- package/lib/index.js.map +1 -0
- package/lib/llm/adapter.d.ts +78 -0
- package/lib/llm/adapter.d.ts.map +1 -0
- package/lib/llm/adapter.js +429 -0
- package/lib/llm/adapter.js.map +1 -0
- package/lib/llm/candidates.d.ts +57 -0
- package/lib/llm/candidates.d.ts.map +1 -0
- package/lib/llm/candidates.js +86 -0
- package/lib/llm/candidates.js.map +1 -0
- package/lib/llm/recovery.d.ts +48 -0
- package/lib/llm/recovery.d.ts.map +1 -0
- package/lib/llm/recovery.js +118 -0
- package/lib/llm/recovery.js.map +1 -0
- package/lib/llm/retry.d.ts +35 -0
- package/lib/llm/retry.d.ts.map +1 -0
- package/lib/llm/retry.js +53 -0
- package/lib/llm/retry.js.map +1 -0
- package/lib/plans.d.ts +59 -0
- package/lib/plans.d.ts.map +1 -0
- package/lib/plans.js +118 -0
- package/lib/plans.js.map +1 -0
- package/lib/session-title.d.ts +63 -0
- package/lib/session-title.d.ts.map +1 -0
- package/lib/session-title.js +280 -0
- package/lib/session-title.js.map +1 -0
- package/lib/settings.d.ts +35 -0
- package/lib/settings.d.ts.map +1 -0
- package/lib/settings.js +78 -0
- package/lib/settings.js.map +1 -0
- package/lib/tools/account.d.ts +11 -0
- package/lib/tools/account.d.ts.map +1 -0
- package/lib/tools/account.js +132 -0
- package/lib/tools/account.js.map +1 -0
- package/lib/tools/billing.d.ts +10 -0
- package/lib/tools/billing.d.ts.map +1 -0
- package/lib/tools/billing.js +100 -0
- package/lib/tools/billing.js.map +1 -0
- package/lib/tools/count-tokens.d.ts +11 -0
- package/lib/tools/count-tokens.d.ts.map +1 -0
- package/lib/tools/count-tokens.js +84 -0
- package/lib/tools/count-tokens.js.map +1 -0
- package/lib/tools/data.d.ts +12 -0
- package/lib/tools/data.d.ts.map +1 -0
- package/lib/tools/data.js +205 -0
- package/lib/tools/data.js.map +1 -0
- package/lib/tools/docs.d.ts +13 -0
- package/lib/tools/docs.d.ts.map +1 -0
- package/lib/tools/docs.js +209 -0
- package/lib/tools/docs.js.map +1 -0
- package/lib/tools/documents.d.ts +10 -0
- package/lib/tools/documents.d.ts.map +1 -0
- package/lib/tools/documents.js +118 -0
- package/lib/tools/documents.js.map +1 -0
- package/lib/tools/media.d.ts +28 -0
- package/lib/tools/media.d.ts.map +1 -0
- package/lib/tools/media.js +599 -0
- package/lib/tools/media.js.map +1 -0
- package/lib/tools/shared.d.ts +89 -0
- package/lib/tools/shared.d.ts.map +1 -0
- package/lib/tools/shared.js +128 -0
- package/lib/tools/shared.js.map +1 -0
- package/lib/tools/x-search.d.ts +11 -0
- package/lib/tools/x-search.d.ts.map +1 -0
- package/lib/tools/x-search.js +129 -0
- package/lib/tools/x-search.js.map +1 -0
- package/lib/web/fallback.d.ts +39 -0
- package/lib/web/fallback.d.ts.map +1 -0
- package/lib/web/fallback.js +79 -0
- package/lib/web/fallback.js.map +1 -0
- package/lib/web/fetch.d.ts +19 -0
- package/lib/web/fetch.d.ts.map +1 -0
- package/lib/web/fetch.js +41 -0
- package/lib/web/fetch.js.map +1 -0
- package/lib/web/search.d.ts +21 -0
- package/lib/web/search.d.ts.map +1 -0
- package/lib/web/search.js +47 -0
- package/lib/web/search.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kenari 路由的模型失败恢复:重试用尽后换模型,再不行回退默认 provider。
|
|
3
|
+
*
|
|
4
|
+
* 分工:5s × 5 的重试交给 dsh 自带的 `dsh-llm-retry`(声明式策略,见 `./retry.ts`),
|
|
5
|
+
* 本模块只负责它放弃之后的升级 —— 因为在同一个 step 内改 provider/model 是 dsh 没提供的缝。
|
|
6
|
+
*
|
|
7
|
+
* 两个监听器的注册顺序是设计的一部分,不是随手写的:
|
|
8
|
+
* 1. `agent/request` 用 `prepend` 挂在**最外层**。瀑布里外层拿最终决定权,而
|
|
9
|
+
* `installModelSelection` 会用「本 step 组装时捕获的」原模型回写 —— 排在内层会被它改回去
|
|
10
|
+
* (`packages/core/agent/src/model-selection.ts:88-105`)。
|
|
11
|
+
* 2. `agent/request-error` 用默认顺序,也就是内层:`dsh-llm-retry` 决定重试时不会 `next()`,
|
|
12
|
+
* 所以我们只在它放弃(码不可重试或次数用尽)之后才被调用。
|
|
13
|
+
* @module dsh-kenari-plugin/llm/recovery
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
17
|
+
import type { ModelCandidates } from './candidates.js';
|
|
18
|
+
/** 一次替换的目标路由。档位用 dsh 的 brand 类型,直接喂回 `agent/request` 才合法。 */
|
|
19
|
+
export interface Target {
|
|
20
|
+
provider: string;
|
|
21
|
+
model: string;
|
|
22
|
+
reasoningEffort?: ReasoningEffortId;
|
|
23
|
+
}
|
|
24
|
+
/** 恢复机制的可调输入与依赖。 */
|
|
25
|
+
export interface ModelRecoveryDeps {
|
|
26
|
+
/** 总开关。false 时一个监听器都不装。 */
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
/** 参与恢复的 provider 路由;范围外的失败原样交给下一个监听者。 */
|
|
29
|
+
providers: readonly string[];
|
|
30
|
+
/** 是否允许换模型;false 时只依赖 dsh 的重试,用尽后该轮以 error 结束。 */
|
|
31
|
+
switchEnabled: boolean;
|
|
32
|
+
/** 换模型/回退前的等待毫秒数。 */
|
|
33
|
+
switchDelayMs: number;
|
|
34
|
+
/** 跳过「同 provider 换模型」、直接回退 provider 的失败码。 */
|
|
35
|
+
skipCodes: readonly string[];
|
|
36
|
+
/** 是否注入模型切换通知。 */
|
|
37
|
+
noticeEnabled: boolean;
|
|
38
|
+
candidates: ModelCandidates;
|
|
39
|
+
/** dsh 默认 provider/model;服务缺失时返回 undefined,回退阶段自动退化为放弃。 */
|
|
40
|
+
defaultSelection: () => Target | undefined;
|
|
41
|
+
logger?: {
|
|
42
|
+
warn(msg: string): void;
|
|
43
|
+
info(msg: string): void;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** 装上恢复机制。调用方负责只在 llm seam 存在时调用(`ctx.inject(['llm'], …)`)。 */
|
|
47
|
+
export declare function installModelRecovery(ctx: Context, deps: ModelRecoveryDeps): void;
|
|
48
|
+
//# sourceMappingURL=recovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../src/llm/recovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAK7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAKtD,6DAA6D;AAC7D,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,eAAe,CAAC,EAAE,iBAAiB,CAAA;CACpC;AAUD,oBAAoB;AACpB,MAAM,WAAW,iBAAiB;IAChC,2BAA2B;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,0CAA0C;IAC1C,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,kDAAkD;IAClD,aAAa,EAAE,OAAO,CAAA;IACtB,qBAAqB;IACrB,aAAa,EAAE,MAAM,CAAA;IACrB,6CAA6C;IAC7C,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,kBAAkB;IAClB,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,eAAe,CAAA;IAC3B,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;IAC1C,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAC9D;AAUD,+DAA+D;AAC/D,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,GAAG,IAAI,CA8FhF"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kenari 路由的模型失败恢复:重试用尽后换模型,再不行回退默认 provider。
|
|
3
|
+
*
|
|
4
|
+
* 分工:5s × 5 的重试交给 dsh 自带的 `dsh-llm-retry`(声明式策略,见 `./retry.ts`),
|
|
5
|
+
* 本模块只负责它放弃之后的升级 —— 因为在同一个 step 内改 provider/model 是 dsh 没提供的缝。
|
|
6
|
+
*
|
|
7
|
+
* 两个监听器的注册顺序是设计的一部分,不是随手写的:
|
|
8
|
+
* 1. `agent/request` 用 `prepend` 挂在**最外层**。瀑布里外层拿最终决定权,而
|
|
9
|
+
* `installModelSelection` 会用「本 step 组装时捕获的」原模型回写 —— 排在内层会被它改回去
|
|
10
|
+
* (`packages/core/agent/src/model-selection.ts:88-105`)。
|
|
11
|
+
* 2. `agent/request-error` 用默认顺序,也就是内层:`dsh-llm-retry` 决定重试时不会 `next()`,
|
|
12
|
+
* 所以我们只在它放弃(码不可重试或次数用尽)之后才被调用。
|
|
13
|
+
* @module dsh-kenari-plugin/llm/recovery
|
|
14
|
+
*/
|
|
15
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
16
|
+
import { isRecoveryProvider, sleepUnlessAborted } from './retry.js';
|
|
17
|
+
/** 决定本次失败后进入哪个阶段;undefined 表示放弃。 */
|
|
18
|
+
function planStage(stage, code, deps) {
|
|
19
|
+
if (!deps.switchEnabled)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (stage === 0)
|
|
22
|
+
return deps.skipCodes.includes(code) ? 2 : 1;
|
|
23
|
+
if (stage === 1)
|
|
24
|
+
return 2;
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/** 装上恢复机制。调用方负责只在 llm seam 存在时调用(`ctx.inject(['llm'], …)`)。 */
|
|
28
|
+
export function installModelRecovery(ctx, deps) {
|
|
29
|
+
if (!deps.enabled)
|
|
30
|
+
return;
|
|
31
|
+
const states = new WeakMap();
|
|
32
|
+
// 最外层:先拿到全链结果再覆盖,才能盖过 installModelSelection 用组装快照做的回写
|
|
33
|
+
ctx.on('agent/request', async ({ agent }, next) => {
|
|
34
|
+
const resolved = await next();
|
|
35
|
+
const target = states.get(agent.session)?.target;
|
|
36
|
+
if (target === undefined)
|
|
37
|
+
return resolved;
|
|
38
|
+
// 档位是 adapter-owned、按模型定义的:把上一个模型的档位搬给新模型可能直接非法,所以丢掉
|
|
39
|
+
const { reasoningEffort: _dropped, ...rest } = resolved;
|
|
40
|
+
return {
|
|
41
|
+
...rest,
|
|
42
|
+
provider: target.provider,
|
|
43
|
+
model: target.model,
|
|
44
|
+
...(target.reasoningEffort === undefined ? {} : { reasoningEffort: target.reasoningEffort }),
|
|
45
|
+
};
|
|
46
|
+
}, { prepend: true });
|
|
47
|
+
ctx.on('agent/request-error', async ({ agent, turn, step, provider, failure, signal }) => {
|
|
48
|
+
if (!isRecoveryProvider(provider, deps.providers))
|
|
49
|
+
return undefined;
|
|
50
|
+
if (signal.aborted)
|
|
51
|
+
return undefined;
|
|
52
|
+
const session = agent.session;
|
|
53
|
+
const current = session.requestContext();
|
|
54
|
+
const currentModel = current?.model;
|
|
55
|
+
if (currentModel === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
const stepKey = `${turn}:${step}`;
|
|
58
|
+
const previous = states.get(session);
|
|
59
|
+
const state = previous?.stepKey === stepKey ? previous : { stepKey, stage: 0 };
|
|
60
|
+
states.set(session, state);
|
|
61
|
+
const planned = planStage(state.stage, failure.code, deps);
|
|
62
|
+
if (planned === undefined) {
|
|
63
|
+
states.delete(session);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
let target;
|
|
67
|
+
let entered = 2;
|
|
68
|
+
if (planned === 1) {
|
|
69
|
+
const candidate = await deps.candidates.pick({
|
|
70
|
+
provider,
|
|
71
|
+
model: currentModel,
|
|
72
|
+
...(current?.contextWindow === undefined ? {} : { contextWindow: current.contextWindow }),
|
|
73
|
+
signal,
|
|
74
|
+
});
|
|
75
|
+
if (candidate !== undefined) {
|
|
76
|
+
target = { provider: candidate.provider, model: candidate.model };
|
|
77
|
+
entered = 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (target === undefined) {
|
|
81
|
+
const fallback = deps.defaultSelection();
|
|
82
|
+
// 已经处于默认路由时「回退」是空操作;重试同一个组合没有意义
|
|
83
|
+
if (fallback === undefined || (fallback.provider === provider && fallback.model === currentModel)) {
|
|
84
|
+
states.delete(session);
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
target = { ...fallback };
|
|
88
|
+
}
|
|
89
|
+
state.stage = entered;
|
|
90
|
+
state.target = target;
|
|
91
|
+
const from = `${provider}/${currentModel}`;
|
|
92
|
+
const to = `${target.provider}/${target.model}`;
|
|
93
|
+
deps.logger?.warn(`kenari: ${from} 失败(${failure.code}),${entered === 1 ? '换模型' : '回退默认 provider'}为 ${to}`);
|
|
94
|
+
if (deps.noticeEnabled) {
|
|
95
|
+
// 换模型对用户是可见的行为变化(风格与能力都会变),静默切换会让人困惑。
|
|
96
|
+
// 形态照抄 dsh 自己的 modelSwitchNotice,走 inject 排到下一个 step 的 pre-step。
|
|
97
|
+
agent.inject(createUserMessage({
|
|
98
|
+
content: [{ type: 'text', text: `[kenari: ${from} 连续失败(${failure.code}),已切换到 ${to}]` }],
|
|
99
|
+
source: { kind: 'plugin', plugin: 'kenari', form: 'notice', summary: `${from} → ${to}` },
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
const waited = await sleepUnlessAborted(deps.switchDelayMs, signal);
|
|
103
|
+
if (!waited) {
|
|
104
|
+
states.delete(session);
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
return { kind: 'retry' };
|
|
108
|
+
});
|
|
109
|
+
// 用户显式选模型(会话日志追加 model/selection)时让人的选择优先。
|
|
110
|
+
// 只有 /model 命令会追加该事件,所以不会误清我们的 override。
|
|
111
|
+
ctx.on('session/event', (session, event) => {
|
|
112
|
+
// `model/selection` 的事件类型由 `@deepseek-ai/dsh-session-controller` 声明合并而来。
|
|
113
|
+
// 为了一个字符串比对上整个 session-controller 依赖不划算,所以按形状读。
|
|
114
|
+
if (event.type === 'model/selection')
|
|
115
|
+
states.delete(session);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../src/llm/recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型失败恢复的共享件:路由判定、重试策略装配、可取消延时。
|
|
3
|
+
*
|
|
4
|
+
* 为什么策略在这里拼:`LlmAdapter.providerRetryPolicy()` 只在适配器注册那一刻被调用一次,
|
|
5
|
+
* 返回值随即被 dsh 冻结进注册记录(`packages/llm/llm/src/index.ts:436-442`)。所以这里产出的
|
|
6
|
+
* 是「注册时的配置快照」—— 改 `modelRetryMaxRetries` 要重启 dsh 才生效,与 plugin-rules #10
|
|
7
|
+
* 对结构性字段的说明一致。
|
|
8
|
+
* @module dsh-kenari-plugin/llm/retry
|
|
9
|
+
*/
|
|
10
|
+
import type { ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
/** 判定一个 provider 路由是否参与模型失败恢复。路由 id 是精确匹配,不做大小写归一。 */
|
|
12
|
+
export declare function isRecoveryProvider(provider: string, configured: readonly string[]): boolean;
|
|
13
|
+
/** 重试策略的可调输入,取自插件 Config。 */
|
|
14
|
+
export interface KenariRetryInput {
|
|
15
|
+
/** 首次请求之后的额外重试次数。 */
|
|
16
|
+
maxRetries?: number;
|
|
17
|
+
/** 每次重试前的固定等待毫秒数。 */
|
|
18
|
+
delayMs?: number;
|
|
19
|
+
/** 允许重试的失败码;省略则用 dsh 默认集,但显式传空数组会被拒绝。 */
|
|
20
|
+
retryableCodes?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 拼出 Kenari 路由的重试策略:固定间隔、无抖动。
|
|
24
|
+
*
|
|
25
|
+
* dsh 的退避是 `initialDelayMs * 2^n` 用 `maxDelayMs` 封顶,所以把两者设成同一个值、
|
|
26
|
+
* 关掉抖动,就得到「每次都等同样久」。「5s 后重试」只有这样才表达得准确。
|
|
27
|
+
*/
|
|
28
|
+
export declare function kenariRetryPolicy(input: KenariRetryInput): ResolvedRetryPolicy;
|
|
29
|
+
/**
|
|
30
|
+
* 可取消的等待。返回 false 表示等待期间被取消,调用方应当放弃本次恢复而不是继续重试。
|
|
31
|
+
*
|
|
32
|
+
* 用 AbortSignal 而不是裸 setTimeout:用户在等待期间点了停止,不该再打一次模型请求。
|
|
33
|
+
*/
|
|
34
|
+
export declare function sleepUnlessAborted(ms: number, signal: AbortSignal): Promise<boolean>;
|
|
35
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/llm/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAqB,MAAM,sBAAsB,CAAA;AAElF,sDAAsD;AACtD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAE3F;AAED,6BAA6B;AAC7B,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yCAAyC;IACzC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACnC;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,mBAAmB,CAU9E;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAapF"}
|
package/lib/llm/retry.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型失败恢复的共享件:路由判定、重试策略装配、可取消延时。
|
|
3
|
+
*
|
|
4
|
+
* 为什么策略在这里拼:`LlmAdapter.providerRetryPolicy()` 只在适配器注册那一刻被调用一次,
|
|
5
|
+
* 返回值随即被 dsh 冻结进注册记录(`packages/llm/llm/src/index.ts:436-442`)。所以这里产出的
|
|
6
|
+
* 是「注册时的配置快照」—— 改 `modelRetryMaxRetries` 要重启 dsh 才生效,与 plugin-rules #10
|
|
7
|
+
* 对结构性字段的说明一致。
|
|
8
|
+
* @module dsh-kenari-plugin/llm/retry
|
|
9
|
+
*/
|
|
10
|
+
import { resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
11
|
+
/** 判定一个 provider 路由是否参与模型失败恢复。路由 id 是精确匹配,不做大小写归一。 */
|
|
12
|
+
export function isRecoveryProvider(provider, configured) {
|
|
13
|
+
return configured.includes(provider);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 拼出 Kenari 路由的重试策略:固定间隔、无抖动。
|
|
17
|
+
*
|
|
18
|
+
* dsh 的退避是 `initialDelayMs * 2^n` 用 `maxDelayMs` 封顶,所以把两者设成同一个值、
|
|
19
|
+
* 关掉抖动,就得到「每次都等同样久」。「5s 后重试」只有这样才表达得准确。
|
|
20
|
+
*/
|
|
21
|
+
export function kenariRetryPolicy(input) {
|
|
22
|
+
const delayMs = input.delayMs ?? 5_000;
|
|
23
|
+
const config = {
|
|
24
|
+
mode: 'normal',
|
|
25
|
+
maxRetries: input.maxRetries ?? 5,
|
|
26
|
+
...(input.retryableCodes === undefined ? {} : { retryableCodes: [...input.retryableCodes] }),
|
|
27
|
+
backoff: { initialDelayMs: delayMs, maxDelayMs: delayMs, jitterRatio: 0 },
|
|
28
|
+
};
|
|
29
|
+
// resolveRetryPolicy 顺带做校验:maxRetries 非负、retryableCodes 非空
|
|
30
|
+
return resolveRetryPolicy(config, 'kenari.modelRecovery.retryPolicy');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 可取消的等待。返回 false 表示等待期间被取消,调用方应当放弃本次恢复而不是继续重试。
|
|
34
|
+
*
|
|
35
|
+
* 用 AbortSignal 而不是裸 setTimeout:用户在等待期间点了停止,不该再打一次模型请求。
|
|
36
|
+
*/
|
|
37
|
+
export function sleepUnlessAborted(ms, signal) {
|
|
38
|
+
if (signal.aborted)
|
|
39
|
+
return Promise.resolve(false);
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
let timer;
|
|
42
|
+
const settle = (waited) => {
|
|
43
|
+
if (timer !== undefined)
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
signal.removeEventListener('abort', onAbort);
|
|
46
|
+
resolve(waited);
|
|
47
|
+
};
|
|
48
|
+
const onAbort = () => settle(false);
|
|
49
|
+
timer = setTimeout(() => settle(true), ms);
|
|
50
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/llm/retry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAGzD,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,UAA6B;IAChF,OAAO,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;AACtC,CAAC;AAYD;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAuB;IACvD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAA;IACtC,MAAM,MAAM,GAAsB;QAChC,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,CAAC;QACjC,GAAG,CAAC,KAAK,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,GAAG,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC;QAC5F,OAAO,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE;KAC1E,CAAA;IACD,2DAA2D;IAC3D,OAAO,kBAAkB,CAAC,MAAM,EAAE,kCAAkC,CAAC,CAAA;AACvE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,EAAU,EAAE,MAAmB;IAChE,IAAI,MAAM,CAAC,OAAO;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;IACjD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,KAAgD,CAAA;QACpD,MAAM,MAAM,GAAG,CAAC,MAAe,EAAQ,EAAE;YACvC,IAAI,KAAK,KAAK,SAAS;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAA;YAC5C,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;YAC5C,OAAO,CAAC,MAAM,CAAC,CAAA;QACjB,CAAC,CAAA;QACD,MAAM,OAAO,GAAG,GAAS,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACzC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;QAC1C,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
package/lib/plans.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kenari 套餐表:`GET /api/plans` 同步 + TTL 缓存 + 模型覆盖索引。
|
|
3
|
+
*
|
|
4
|
+
* 这是**站点**端点(不在 `/v1` 下、不需要 key),官网的「Langganan」页就是读它渲染的;
|
|
5
|
+
* 每个套餐给一份 `scope_models`——请求命中覆盖列表里的模型才从套餐额度扣,
|
|
6
|
+
* 否则直接走预付余额。所以「这个模型哪些套餐能用」只有一个事实来源,就是这里。
|
|
7
|
+
*
|
|
8
|
+
* 形状比 API 线宽松:字段缺失、整体 404、官网改版都按"套餐数据不可用"处理,
|
|
9
|
+
* 需要它的界面降级为只显示能力标签,而不是把插件判为故障。
|
|
10
|
+
* @module dsh-kenari-plugin/plans
|
|
11
|
+
*/
|
|
12
|
+
import type { KenariHttpDeps } from './http.js';
|
|
13
|
+
/** 一个订阅套餐:价格与它覆盖的模型列表。 */
|
|
14
|
+
export interface KenariPlan {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
/** 价格(IDR);0 表示免费档。目录未公布时为 undefined。 */
|
|
18
|
+
priceIdr?: number;
|
|
19
|
+
/** 覆盖的模型 id(裸 id,不带 `:free` 后缀)。 */
|
|
20
|
+
scopeModels: string[];
|
|
21
|
+
/** 该套餐下缓存读取不计额度的模型 id。 */
|
|
22
|
+
freeCacheModels: string[];
|
|
23
|
+
}
|
|
24
|
+
/** 模型 id 的套餐归属:套餐名列表(含免费缓存标记)。 */
|
|
25
|
+
export interface PlanCoverage {
|
|
26
|
+
/** 覆盖该模型的套餐名,按套餐表顺序。 */
|
|
27
|
+
plans: string[];
|
|
28
|
+
/** 其中对该模型免缓存读额度的套餐名。 */
|
|
29
|
+
freeCachePlans: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 套餐表客户端:进程内 TTL 缓存(复用 `catalogCacheTtlMs`——两者都是"Kenari 的目录类事实")。
|
|
33
|
+
* 并发请求合并到同一次抓取。
|
|
34
|
+
*/
|
|
35
|
+
export declare class KenariPlans {
|
|
36
|
+
private readonly deps;
|
|
37
|
+
private cache;
|
|
38
|
+
private inflight;
|
|
39
|
+
constructor(deps: KenariHttpDeps);
|
|
40
|
+
/** 缓存有效期取自实时配置:设置页改完下一次读取即生效。 */
|
|
41
|
+
private get ttlMs();
|
|
42
|
+
/**
|
|
43
|
+
* 抓取套餐表(公开端点,无需 key)。
|
|
44
|
+
* 失败上抛:调用方按"套餐数据缺失"降级,不要让它挡住模型目录。
|
|
45
|
+
*/
|
|
46
|
+
list(signal?: AbortSignal): Promise<KenariPlan[]>;
|
|
47
|
+
/**
|
|
48
|
+
* 模型 id → 套餐归属索引。
|
|
49
|
+
*
|
|
50
|
+
* 索引键同时收裸 id 与 `:free` 形式:套餐表写 `hy3`,目录里 `hy3` 与 `hy3:free`
|
|
51
|
+
* 都可能是可路由的模型,两者都该算被覆盖。
|
|
52
|
+
*/
|
|
53
|
+
coverage(signal?: AbortSignal): Promise<Map<string, PlanCoverage>>;
|
|
54
|
+
/** 清空缓存(测试与强制刷新用)。 */
|
|
55
|
+
invalidate(): void;
|
|
56
|
+
}
|
|
57
|
+
/** 覆盖查询:先按原 id,再按裸 id(目录带 `:free`、套餐表不带的场景)。 */
|
|
58
|
+
export declare function coverageOf(index: Map<string, PlanCoverage>, modelId: string): PlanCoverage;
|
|
59
|
+
//# sourceMappingURL=plans.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAK/C,0BAA0B;AAC1B,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oCAAoC;IACpC,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,0BAA0B;IAC1B,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B;AAED,kCAAkC;AAClC,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,wBAAwB;IACxB,cAAc,EAAE,MAAM,EAAE,CAAA;CACzB;AA0BD;;;GAGG;AACH,qBAAa,WAAW;IAIV,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,KAAK,CAAwD;IACrE,OAAO,CAAC,QAAQ,CAAmC;gBAEtB,IAAI,EAAE,cAAc;IAEjD,iCAAiC;IACjC,OAAO,KAAK,KAAK,GAEhB;IAED;;;OAGG;IACG,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAyBvD;;;;;OAKG;IACG,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAkBxE,sBAAsB;IACtB,UAAU,IAAI,IAAI;CAGnB;AAED,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAE1F"}
|
package/lib/plans.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kenari 套餐表:`GET /api/plans` 同步 + TTL 缓存 + 模型覆盖索引。
|
|
3
|
+
*
|
|
4
|
+
* 这是**站点**端点(不在 `/v1` 下、不需要 key),官网的「Langganan」页就是读它渲染的;
|
|
5
|
+
* 每个套餐给一份 `scope_models`——请求命中覆盖列表里的模型才从套餐额度扣,
|
|
6
|
+
* 否则直接走预付余额。所以「这个模型哪些套餐能用」只有一个事实来源,就是这里。
|
|
7
|
+
*
|
|
8
|
+
* 形状比 API 线宽松:字段缺失、整体 404、官网改版都按"套餐数据不可用"处理,
|
|
9
|
+
* 需要它的界面降级为只显示能力标签,而不是把插件判为故障。
|
|
10
|
+
* @module dsh-kenari-plugin/plans
|
|
11
|
+
*/
|
|
12
|
+
import { kenariPublicGet, siteEndpoint } from './http.js';
|
|
13
|
+
/** 套餐端点路径(相对裸域)。 */
|
|
14
|
+
const PLANS_PATH = '/api/plans';
|
|
15
|
+
/** 去掉 `:free` 后缀:套餐表用裸 id,目录用带后缀的 id,join 前必须归一。 */
|
|
16
|
+
function bareId(id) {
|
|
17
|
+
return id.endsWith(':free') ? id.slice(0, -5) : id;
|
|
18
|
+
}
|
|
19
|
+
/** 宽松解析一行套餐;缺 id/name 的行直接丢弃(不猜)。 */
|
|
20
|
+
function parsePlan(row) {
|
|
21
|
+
if (typeof row !== 'object' || row === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
const record = row;
|
|
24
|
+
const id = record.id;
|
|
25
|
+
const name = record.name;
|
|
26
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
const strings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
name,
|
|
34
|
+
...(typeof record.price_idr === 'number' ? { priceIdr: record.price_idr } : {}),
|
|
35
|
+
scopeModels: strings(record.scope_models),
|
|
36
|
+
freeCacheModels: strings(record.free_cache_models),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 套餐表客户端:进程内 TTL 缓存(复用 `catalogCacheTtlMs`——两者都是"Kenari 的目录类事实")。
|
|
41
|
+
* 并发请求合并到同一次抓取。
|
|
42
|
+
*/
|
|
43
|
+
export class KenariPlans {
|
|
44
|
+
deps;
|
|
45
|
+
cache;
|
|
46
|
+
inflight;
|
|
47
|
+
constructor(deps) {
|
|
48
|
+
this.deps = deps;
|
|
49
|
+
}
|
|
50
|
+
/** 缓存有效期取自实时配置:设置页改完下一次读取即生效。 */
|
|
51
|
+
get ttlMs() {
|
|
52
|
+
return this.deps.config.catalogCacheTtlMs ?? 3_600_000;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 抓取套餐表(公开端点,无需 key)。
|
|
56
|
+
* 失败上抛:调用方按"套餐数据缺失"降级,不要让它挡住模型目录。
|
|
57
|
+
*/
|
|
58
|
+
async list(signal) {
|
|
59
|
+
const cached = this.cache;
|
|
60
|
+
const ttlMs = this.ttlMs;
|
|
61
|
+
if (ttlMs > 0 && cached !== undefined && Date.now() - cached.fetchedAt < ttlMs) {
|
|
62
|
+
return cached.plans;
|
|
63
|
+
}
|
|
64
|
+
if (this.inflight !== undefined)
|
|
65
|
+
return this.inflight;
|
|
66
|
+
const request = kenariPublicGet(this.deps, siteEndpoint(this.deps.config.baseURL, PLANS_PATH), signal, { retries: 1 })
|
|
67
|
+
.then((payload) => {
|
|
68
|
+
const rows = Array.isArray(payload)
|
|
69
|
+
? payload
|
|
70
|
+
: (typeof payload === 'object' && payload !== null && Array.isArray(payload.data)
|
|
71
|
+
? payload.data
|
|
72
|
+
: []);
|
|
73
|
+
const plans = rows.map(parsePlan).filter((plan) => plan !== undefined);
|
|
74
|
+
this.cache = { plans, fetchedAt: Date.now() };
|
|
75
|
+
return plans;
|
|
76
|
+
})
|
|
77
|
+
.finally(() => {
|
|
78
|
+
this.inflight = undefined;
|
|
79
|
+
});
|
|
80
|
+
this.inflight = request;
|
|
81
|
+
return request;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 模型 id → 套餐归属索引。
|
|
85
|
+
*
|
|
86
|
+
* 索引键同时收裸 id 与 `:free` 形式:套餐表写 `hy3`,目录里 `hy3` 与 `hy3:free`
|
|
87
|
+
* 都可能是可路由的模型,两者都该算被覆盖。
|
|
88
|
+
*/
|
|
89
|
+
async coverage(signal) {
|
|
90
|
+
const plans = await this.list(signal);
|
|
91
|
+
const index = new Map();
|
|
92
|
+
const record = (id, planName, freeCache) => {
|
|
93
|
+
for (const candidate of id.endsWith(':free') ? [id] : [id, `${id}:free`]) {
|
|
94
|
+
const entry = index.get(candidate) ?? { plans: [], freeCachePlans: [] };
|
|
95
|
+
const bucket = freeCache ? entry.freeCachePlans : entry.plans;
|
|
96
|
+
if (!bucket.includes(planName))
|
|
97
|
+
bucket.push(planName);
|
|
98
|
+
index.set(candidate, entry);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
for (const plan of plans) {
|
|
102
|
+
for (const id of plan.scopeModels)
|
|
103
|
+
record(id, plan.name, false);
|
|
104
|
+
for (const id of plan.freeCacheModels)
|
|
105
|
+
record(id, plan.name, true);
|
|
106
|
+
}
|
|
107
|
+
return index;
|
|
108
|
+
}
|
|
109
|
+
/** 清空缓存(测试与强制刷新用)。 */
|
|
110
|
+
invalidate() {
|
|
111
|
+
this.cache = undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 覆盖查询:先按原 id,再按裸 id(目录带 `:free`、套餐表不带的场景)。 */
|
|
115
|
+
export function coverageOf(index, modelId) {
|
|
116
|
+
return index.get(modelId) ?? index.get(bareId(modelId)) ?? { plans: [], freeCachePlans: [] };
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=plans.js.map
|
package/lib/plans.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plans.js","sourceRoot":"","sources":["../src/plans.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话标题前缀:`yyyyMMddHHmmss-`(模板可自定义,可开关)。
|
|
3
|
+
*
|
|
4
|
+
* dsh 的标题是 log-only 的 `session/title` 事件,事件数据固定为
|
|
5
|
+
* `{ title, messageSeqs, source }` —— 没有独立的前缀字段,前缀只能是 title
|
|
6
|
+
* 字符串的一部分。所以本模块在服务端事件层统一改写:剥掉旧前缀、按本会话的
|
|
7
|
+
* 开始时间(本机时区)重算,再 append 一条同源事件;标题取最后一条,因此 Web、
|
|
8
|
+
* TUI、headless 读到的是同一个字符串。
|
|
9
|
+
*
|
|
10
|
+
* 开始时间取「本会话第一条人类 `user/message` 的时间」,而不是
|
|
11
|
+
* `session.header.createdAt`:dsh 的 Web 侧会**复用空白会话**,记录可能在工作区
|
|
12
|
+
* 打开时就建好,比真正开口说话早几个小时,用 createdAt 会写出对不上的时间。
|
|
13
|
+
* 没有任何人类消息时(例如对空会话手动改名)才退回 `header.createdAt`;fork
|
|
14
|
+
* 子会话跳过 seed 继承的事件,所以拿到的是子会话自己的第一条消息。
|
|
15
|
+
*
|
|
16
|
+
* 不注册 provider、不改 dsh 行:监听器挂在 bundle 插件 ctx 上(未打 scope 标签的
|
|
17
|
+
* 上下文全局收得到会话事件),只在标题事件与 fork 创建时动手。自动标题
|
|
18
|
+
* (provider / fallback)与手动 rename 一视同仁 —— 浏览器 fork 之后会用一次
|
|
19
|
+
* `source.kind === 'user'` 的 rename 把父标题带进子会话,按 source 区分会在 fork
|
|
20
|
+
* 上漏改前缀。
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-kenari-plugin/session-title
|
|
23
|
+
*/
|
|
24
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
25
|
+
/** 前缀的可调配置。全部用 thunk 读,改动在下一次标题事件即生效。 */
|
|
26
|
+
export interface SessionTitlePrefixOptions {
|
|
27
|
+
/** 是否在生成标题时写入前缀。 */
|
|
28
|
+
readonly enabled: () => boolean;
|
|
29
|
+
/** 前缀模板:token `yyyy/MM/dd/HH/mm/ss`(本机时区),其余字符原样。 */
|
|
30
|
+
readonly template: () => string;
|
|
31
|
+
/** 前缀 + 正文的 UTF-8 总字节预算。 */
|
|
32
|
+
readonly maxBytes: () => number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 按会话创建时间渲染前缀模板。
|
|
36
|
+
* @param template - 前缀模板。
|
|
37
|
+
* @param createdAt - 会话创建时刻(epoch 毫秒)。
|
|
38
|
+
* @returns 渲染后的前缀;不含 token 的模板即模板本身。
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderTitlePrefix(template: string, createdAt: number): string;
|
|
41
|
+
/**
|
|
42
|
+
* 写入设置页时的本地校验:schema 表达不了的约束在这里拦下。
|
|
43
|
+
* @param options - 当前配置值。
|
|
44
|
+
* @throws 模板为空/渲染为空/含控制字符/过长,或字节预算容不下前缀加最短正文时。
|
|
45
|
+
*/
|
|
46
|
+
export declare function validateSessionTitlePrefix(options: {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
template: string;
|
|
49
|
+
maxBytes: number;
|
|
50
|
+
}): void;
|
|
51
|
+
/**
|
|
52
|
+
* 在服务端标题事件上维护前缀。
|
|
53
|
+
*
|
|
54
|
+
* 每次标题事件与每次 fork 创建后:剥掉标题开头的一个已知前缀(本会话上次写入的、
|
|
55
|
+
* 父会话的、以及历史版本按记录创建时间写下的),开关开启时再拼上按本会话开始时间
|
|
56
|
+
* 渲染的前缀,然后 append 一条同源事件。同一标题重复触发时结果与当前一致,不会
|
|
57
|
+
* 追加事件。
|
|
58
|
+
*
|
|
59
|
+
* @param ctx - bundle 插件上下文;监听器随 fiber 卸载自动回收。
|
|
60
|
+
* @param options - 前缀开关、模板与字节预算。
|
|
61
|
+
*/
|
|
62
|
+
export declare function installSessionTitlePrefix(ctx: Context, options: SessionTitlePrefixOptions): void;
|
|
63
|
+
//# sourceMappingURL=session-title.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-title.d.ts","sourceRoot":"","sources":["../src/session-title.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AA0ClD,yCAAyC;AACzC,MAAM,WAAW,yBAAyB;IACxC,oBAAoB;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAA;IAC/B,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAA;IAC/B,4BAA4B;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,MAAM,CAAA;CAChC;AAUD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAc7E;AA4ED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,IAAI,CAsBP;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,yBAAyB,GAAG,IAAI,CAsGhG"}
|