@morlay/dsh-session-mode 0.0.6 → 0.0.8
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 +33 -16
- package/dist/catalog-remote-B-NMi_vS.mjs +21 -0
- package/dist/client.cjs +2623 -1714
- package/dist/client.d.cts +473 -4
- package/dist/client.d.mts +473 -4
- package/dist/index.d.cts +3 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +160 -24
- package/dist/rows.mjs +39 -28
- package/package.json +15 -3
- package/src/catalog-remote.ts +45 -0
- package/src/catalog.ts +75 -0
- package/src/client/BundleConfigPage.tsx +630 -0
- package/src/client/SessionModeSeat.tsx +21 -19
- package/src/client/bundle-config.ts +701 -0
- package/src/client/bundle-locales.ts +214 -0
- package/src/client/index.ts +180 -72
- package/src/client/llm-directory.ts +146 -0
- package/src/client/locales.ts +2 -2
- package/src/index.ts +25 -11
- package/src/mode-sources.ts +20 -6
- package/src/modes.ts +46 -21
- package/src/rows.ts +3 -2
- package/src/scope.ts +4 -5
- package/dist/shared-DX2Giyh3.mjs +0 -5
- package/src/client/SessionModeSeat.module.css +0 -74
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// 部署的 LLM 目录读数:provider 候选(活着的路由 + 可配置声明)与每个 provider 的模型清单(在声明指向的那份
|
|
2
|
+
// 配置里)。
|
|
3
|
+
//
|
|
4
|
+
// 两处消费它:提示面的 `llm-providers` / `llm-models` 两个具名源(行配置页与通用 schema 表单用),以及 bundle
|
|
5
|
+
// 配置页的模型路由清单——"已设置的模型"按**路由**给,一条路由就是 provider + model 这一对。
|
|
6
|
+
|
|
7
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
8
|
+
import type {} from "@deepseek-ai/dsh-api-remotes/client";
|
|
9
|
+
|
|
10
|
+
// 一条可选路由:`key` 是稳定身份(调用方只用来比对,不解析它)。
|
|
11
|
+
export interface ModelRoute {
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly provider: string;
|
|
14
|
+
readonly providerName: string;
|
|
15
|
+
readonly model: string;
|
|
16
|
+
readonly modelName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 一个 provider 与它当前可用的模型。
|
|
20
|
+
export interface ProviderModels {
|
|
21
|
+
readonly value: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly models: readonly { readonly value: string; readonly label: string }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 路由的稳定身份:provider 与 model 一起才是路由。
|
|
27
|
+
export function routeKey(provider: string, model: string): string {
|
|
28
|
+
return `${provider}\u0000${model}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 客户端 remote 的命名空间是一个**独立服务**(服务名 `remote.<命名空间>`,gateway 的 client 半按 mount 注册),
|
|
32
|
+
// 不是 `remote` 服务上的属性——属性访问读到的是 `undefined`。读不到(这一版部署没装那个命名空间)就按"没有"处理。
|
|
33
|
+
export function remoteNamespace<T>(scope: Context, namespace: string): T | undefined {
|
|
34
|
+
try {
|
|
35
|
+
return (scope as unknown as { get(key: string): unknown }).get(`remote.${namespace}`) as
|
|
36
|
+
| T
|
|
37
|
+
| undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
// 服务没注册时 `ctx.get` 抛错:按"这个命名空间不在这一版部署里"处理。
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// LLM 命名空间里本包用到的那两个读面:活着的路由与可配置声明。
|
|
45
|
+
interface LlmReader {
|
|
46
|
+
listProviders: () => Promise<
|
|
47
|
+
{ ok: true; value: readonly { id: string; name: string }[] } | { ok: false }
|
|
48
|
+
>;
|
|
49
|
+
listConfigurableProviders: () => Promise<
|
|
50
|
+
| {
|
|
51
|
+
ok: true;
|
|
52
|
+
value: readonly {
|
|
53
|
+
provider: string;
|
|
54
|
+
displayName: string;
|
|
55
|
+
settingsNs: string;
|
|
56
|
+
settingsPath: readonly string[];
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
| { ok: false }
|
|
60
|
+
>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 一份 provider 档案的读数面(模型清单住在声明指向的那份配置里)。
|
|
64
|
+
interface ConfigFormsReader {
|
|
65
|
+
get(ns: string): { getSnapshot(): { value: unknown } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 按路径读一段配置里的值。
|
|
69
|
+
function readAt(root: unknown, path: readonly string[]): unknown {
|
|
70
|
+
return path.reduce<unknown>((node, segment) => {
|
|
71
|
+
if (Array.isArray(node)) return node[Number(segment)];
|
|
72
|
+
if (typeof node !== "object" || node === null) return undefined;
|
|
73
|
+
return Reflect.get(node, segment);
|
|
74
|
+
}, root);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 一个 provider 当前有哪些模型:声明给了配置地址就按路径读它的 `models`(每项取 `id`)。
|
|
78
|
+
function modelsOf(
|
|
79
|
+
entry: { settingsNs: string; settingsPath: readonly string[] },
|
|
80
|
+
forms: ConfigFormsReader,
|
|
81
|
+
): readonly { value: string; label: string }[] {
|
|
82
|
+
if (entry.settingsNs === "") return [];
|
|
83
|
+
const profile = readAt(forms.get(entry.settingsNs).getSnapshot().value, entry.settingsPath);
|
|
84
|
+
const models =
|
|
85
|
+
typeof profile === "object" && profile !== null ? Reflect.get(profile, "models") : undefined;
|
|
86
|
+
if (!Array.isArray(models)) return [];
|
|
87
|
+
return models.flatMap((model) => {
|
|
88
|
+
if (typeof model !== "object" || model === null) return [];
|
|
89
|
+
const id = Reflect.get(model, "id");
|
|
90
|
+
if (typeof id !== "string") return [];
|
|
91
|
+
// 显示名优先取档案里的 `name`("DeepSeek V4.1 Flash @ Ollama Cloud" 这种),没有就用 id。
|
|
92
|
+
const name = Reflect.get(model, "name");
|
|
93
|
+
return [{ value: id, label: typeof name === "string" && name !== "" ? name : id }];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 部署里"已设置"的 provider 与它们的模型:活着的路由与可配置声明合并(按 provider 去重,声明的显示名优先)。
|
|
98
|
+
export async function loadLlmProviders(scope: Context): Promise<readonly ProviderModels[]> {
|
|
99
|
+
const llm = remoteNamespace<LlmReader>(scope, "llm");
|
|
100
|
+
const forms = (scope as unknown as { get(key: string): unknown }).get(
|
|
101
|
+
"configForms",
|
|
102
|
+
) as ConfigFormsReader | undefined;
|
|
103
|
+
if (llm === undefined || forms === undefined) return [];
|
|
104
|
+
const [routes, directory] = await Promise.all([
|
|
105
|
+
llm.listProviders(),
|
|
106
|
+
llm.listConfigurableProviders(),
|
|
107
|
+
]);
|
|
108
|
+
if (!routes.ok || !directory.ok) return [];
|
|
109
|
+
const merged = new Map<string, { value: string; label: string; settingsNs: string; settingsPath: readonly string[] }>();
|
|
110
|
+
for (const entry of directory.value) {
|
|
111
|
+
merged.set(entry.provider, {
|
|
112
|
+
value: entry.provider,
|
|
113
|
+
label: entry.displayName,
|
|
114
|
+
settingsNs: entry.settingsNs,
|
|
115
|
+
settingsPath: [...entry.settingsPath],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const route of routes.value) {
|
|
119
|
+
if (!merged.has(route.id)) {
|
|
120
|
+
merged.set(route.id, {
|
|
121
|
+
value: route.id,
|
|
122
|
+
label: route.name,
|
|
123
|
+
settingsNs: "",
|
|
124
|
+
settingsPath: [],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [...merged.values()].map((entry) => ({
|
|
129
|
+
value: entry.value,
|
|
130
|
+
label: entry.label,
|
|
131
|
+
models: modelsOf(entry, forms),
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 目录里的每一条路由(provider + 它有模型的每一项):bundle 配置页的模型清单用它。
|
|
136
|
+
export function routesOf(providers: readonly ProviderModels[]): readonly ModelRoute[] {
|
|
137
|
+
return providers.flatMap((provider) =>
|
|
138
|
+
provider.models.map((model) => ({
|
|
139
|
+
key: routeKey(provider.value, model.value),
|
|
140
|
+
provider: provider.value,
|
|
141
|
+
providerName: provider.label,
|
|
142
|
+
model: model.value,
|
|
143
|
+
modelName: model.label,
|
|
144
|
+
})),
|
|
145
|
+
);
|
|
146
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const zh = {
|
|
|
11
11
|
model: "模型",
|
|
12
12
|
modelHint: "模型 id。",
|
|
13
13
|
reasoningEffort: "思考档位",
|
|
14
|
-
reasoningEffortHint: "
|
|
14
|
+
reasoningEffortHint: "默认就跟服务商自己的默认。",
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export const en: Record<keyof typeof zh, string> = {
|
|
@@ -24,7 +24,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
24
24
|
model: "Model",
|
|
25
25
|
modelHint: "Model id.",
|
|
26
26
|
reasoningEffort: "Reasoning effort",
|
|
27
|
-
reasoningEffortHint: "
|
|
27
|
+
reasoningEffortHint: "Default keeps the provider's own default.",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export type SessionModeLocaleKey = keyof typeof zh;
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type SessionMode,
|
|
26
26
|
type SessionModeRole,
|
|
27
27
|
} from "./modes.ts";
|
|
28
|
+
import { SessionModeCatalog } from "./catalog.ts";
|
|
28
29
|
import { installPersona } from "./persona.ts";
|
|
29
30
|
import { SessionScope } from "./scope.ts";
|
|
30
31
|
import {
|
|
@@ -263,8 +264,8 @@ export class SessionModes extends Service {
|
|
|
263
264
|
return undefined;
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
// 把某个空白会话(必须还没开过 turn
|
|
267
|
-
//
|
|
267
|
+
// 把某个空白会话(必须还没开过 turn)切到某个模式:先按模式的白名单把 agent preset 落到位(当前那份已经在
|
|
268
|
+
// 名单里就不换——换 preset 是一次重挂;不在名单里就换成名单里的第一个),再落会话事实并重装该 agent 的扩展。
|
|
268
269
|
async select(sessionId: SessionId, mode: string): Promise<string> {
|
|
269
270
|
const definition = this.definition(mode);
|
|
270
271
|
if (!definition.role.includes("main")) {
|
|
@@ -278,11 +279,12 @@ export class SessionModes extends Service {
|
|
|
278
279
|
}
|
|
279
280
|
const agent = this.ctx.agents.get(sessionId);
|
|
280
281
|
const registry = this.presetRegistry();
|
|
281
|
-
if (agent !== undefined && registry !== undefined
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
if (agent !== undefined && registry !== undefined) {
|
|
283
|
+
const target = this.presetTarget(definition.presetsOnly, this.presetOfAgent(registry, agent));
|
|
284
|
+
if (target !== undefined) {
|
|
285
|
+
// 官方那条路自己也会查空白窗口(`agent-preset/locked`);它切完会 emit `agent-preset/selected`,
|
|
286
|
+
// 监听据此落事实并重装——所以这里先比一次投影,同一个值不写第二条。
|
|
287
|
+
await registry.select(agent, target);
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
if (this.ctx.sessionProjections.stateOf(session, "sessionMode") !== mode) {
|
|
@@ -293,6 +295,16 @@ export class SessionModes extends Service {
|
|
|
293
295
|
return mode;
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
// 把当前挂着的 preset 落进模式的白名单:已经在名单里就返回 `undefined`(不换);不在里面(或读不到当前那份)
|
|
299
|
+
// 就返回名单的**第一个**(`undefined` = 名单为空 = 不限制,什么都不换)。
|
|
300
|
+
private presetTarget(allowed: readonly string[], current: string | undefined): string | undefined {
|
|
301
|
+
if (allowed.length === 0) return undefined;
|
|
302
|
+
// 当前那份在名单里:保持不动(用户自己选的 preset 落在许可范围内,模式不去覆盖它)。
|
|
303
|
+
if (current !== undefined && allowed.includes(current)) return undefined;
|
|
304
|
+
const target = allowed[0];
|
|
305
|
+
return target === current ? undefined : target;
|
|
306
|
+
}
|
|
307
|
+
|
|
296
308
|
// 某个 agent 当前挂着的 preset(registry 的 `composedPreset`);读不到时按"未知"处理(该切就切)。
|
|
297
309
|
private presetOfAgent(registry: Context["agentPresets"], agent: Agent): string | undefined {
|
|
298
310
|
const read = (registry as { composedPreset?: (ctx: Context) => string | undefined })
|
|
@@ -355,12 +367,12 @@ export class SessionModes extends Service {
|
|
|
355
367
|
}
|
|
356
368
|
}
|
|
357
369
|
|
|
358
|
-
// 某个 preset 对应的模式 id
|
|
359
|
-
//
|
|
370
|
+
// 某个 preset 对应的模式 id——只在映射唯一时回答(只有它一个模式的名单里有这份 preset;没有模式认领、或几个
|
|
371
|
+
// 模式的名单都含它时 `undefined`);本部署的模式由会话事实决定,这条反查留给"一对一映射"的部署形态。
|
|
360
372
|
modeForPreset(preset: string | undefined): string | undefined {
|
|
361
373
|
if (preset === undefined) return undefined;
|
|
362
|
-
const owners = Object.keys(this.config.modes).filter(
|
|
363
|
-
|
|
374
|
+
const owners = Object.keys(this.config.modes).filter((id) =>
|
|
375
|
+
this.config.modes[id]?.presetsOnly.includes(preset),
|
|
364
376
|
);
|
|
365
377
|
return owners.length === 1 ? owners[0] : undefined;
|
|
366
378
|
}
|
|
@@ -429,6 +441,8 @@ export class SessionModes extends Service {
|
|
|
429
441
|
export function apply(ctx: Context, config: ResolvedConfig): void {
|
|
430
442
|
const modes = new SessionModes(ctx, config);
|
|
431
443
|
registerHttpRoutes(ctx, modes);
|
|
444
|
+
// 配置页的工具 / 技能候选:一条本包自己开的 Remote 面(客户端半自己 mount,见 `./catalog-remote.ts`)。
|
|
445
|
+
ctx.plugin(SessionModeCatalog);
|
|
432
446
|
}
|
|
433
447
|
|
|
434
448
|
interface HttpRequestLike {
|
package/src/mode-sources.ts
CHANGED
|
@@ -7,10 +7,10 @@ import type { PolicyName } from "./shared.ts";
|
|
|
7
7
|
// 一个模式的源定义:就是 `session-mode` 行 `config.modes` 里的一项。
|
|
8
8
|
export interface ModeSource {
|
|
9
9
|
readonly id: string;
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
readonly
|
|
10
|
+
// 这个模式**只允许**挂哪些 agent preset(它们的 `id`):**可选**,不写(留空)就是不限制——选这个模式不换 preset,
|
|
11
|
+
// 会话保持它当前挂着的那份,行清单由那份 preset 提供(官方四个 shipped preset 照旧可选)。有值时切模式会让 preset
|
|
12
|
+
// 落进名单:当前的已经在里面就不动,否则换成第一个。模式定义里可以整体改写它(用户 patch 层)。
|
|
13
|
+
readonly presetsOnly?: readonly string[];
|
|
14
14
|
readonly name: string;
|
|
15
15
|
readonly description: string;
|
|
16
16
|
// 归谁用:`main`(用户选择器,缺省)、`subagent`(可作子代理 mode 的候选)。
|
|
@@ -59,11 +59,16 @@ const CHAT_PERSONA = {
|
|
|
59
59
|
"你是一个助手。全程用中文(专有名词除外),包括但不限于思考,回答,工具描述;思考不要陷入重复循环,一旦循环立即退出。",
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
// 原样模式:**一份什么都不加的扩展**——persona 空、名单全空、三个开关不写,装配结果与"没有模式"时一致。
|
|
63
|
+
// 它存在的理由有两个:想完全按上游默认跑的会话有个可选项;排查扩展干扰时有一个对照档(切到它就只剩上游行为)。
|
|
64
|
+
// 它同样是 `session-mode` 行的一份定义,所以模式自己的收口代码照常经过它——只是没有任何收口要求。
|
|
65
|
+
const NOOP_MODE_ID = "noop";
|
|
66
|
+
|
|
62
67
|
// 新会话用哪个模式(`session-mode` 行的 `config.default`)。
|
|
63
68
|
export const DEFAULT_MODE = "coding";
|
|
64
69
|
|
|
65
|
-
//
|
|
66
|
-
// 不收窄(用全部),`chat` 收成提问 +
|
|
70
|
+
// 三个模式:编码、对话与原样。**都不限制 preset**(差异全在会话级收口),行清单由会话挂着的那份提供:`coding`
|
|
71
|
+
// 不收窄(用全部),`chat` 收成提问 + 联网三件,`noop` 什么都不加。
|
|
67
72
|
export const MODE_SOURCES: readonly ModeSource[] = [
|
|
68
73
|
{
|
|
69
74
|
id: "coding",
|
|
@@ -104,4 +109,13 @@ export const MODE_SOURCES: readonly ModeSource[] = [
|
|
|
104
109
|
instructions: false,
|
|
105
110
|
runtimeContext: false,
|
|
106
111
|
},
|
|
112
|
+
{
|
|
113
|
+
id: NOOP_MODE_ID,
|
|
114
|
+
name: "原样模式",
|
|
115
|
+
description: "与上游一致:不加人格提示词、不收窄工具与技能、policy 规则全开、官方注入面照旧。",
|
|
116
|
+
// 用户可选,也允许作为子代理的 mode:它不做任何过滤,给子代理当候选同样成立。
|
|
117
|
+
role: ["main", "subagent"],
|
|
118
|
+
// 其余字段一律不写:名单留空 = 不设收窄,`instructions` / `runtimeContext` 走 schema 默认 `true`,
|
|
119
|
+
// `skills` 由工具名单推导(`allowTools` 留空 → 含 `skill` → 要目录),`presetsOnly` 留空 = 不限制、`defaultModel` 跟全局。
|
|
120
|
+
},
|
|
107
121
|
];
|
package/src/modes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 模式的定义形状与它的装配期校验:一个模式就是"一段提示词 + 一组能力开关",本行的 `config.modes` 声明它们。
|
|
2
|
-
//
|
|
2
|
+
// 行清单来自会话挂着的那份 agent preset(`presetsOnly` 只声明这个模式允许挂哪些);装配层能整体改写 `modes`,
|
|
3
|
+
// 也可以只给某几个模式换提示词或白名单。
|
|
3
4
|
// 取舍见 `.agents/adrs/20260925-默认模型住在模式定义里.md`、`.agents/designs/20260924-会话模式.md`。
|
|
4
5
|
|
|
5
6
|
import type { Volatile } from "@deepseek-ai/cordis";
|
|
@@ -31,11 +32,12 @@ export type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
|
|
|
31
32
|
// 一个模式:提示词 + 能力开关。这份形状是 **schema 归一化之后**的(每个字段都有值,schema 用默认补上),
|
|
32
33
|
// 配置里能省略哪些字段看 `modeSchema` 的 default。
|
|
33
34
|
export interface SessionMode {
|
|
34
|
-
//
|
|
35
|
-
//
|
|
35
|
+
// 这个模式**只允许**挂哪些 **agent preset**(官方四个,或本部署自己注册的那一份)。**留空(不写)表示不限制**:
|
|
36
|
+
// 选这个模式不换 preset,会话保持它当前挂着的那份;行清单由那份 preset 提供,模式自己的那几项扩展
|
|
36
37
|
// (persona / 工具名单 / policy 名单 / 三个开关 / `defaultModel`)在任意 preset 上都照常生效。
|
|
37
|
-
//
|
|
38
|
-
|
|
38
|
+
// 有值时切到这个模式会让会话的 preset 落进名单:当前那份已经在名单里就原样不动,否则换成名单里的**第一个**
|
|
39
|
+
// (`modes.ts` 只声明"允许哪些",落在哪个由 `SessionModes.select` 决定)。多个模式可以共享同一份名单。
|
|
40
|
+
readonly presetsOnly: string[];
|
|
39
41
|
// 模式的展示名(选择面归官方 roster;这里留着做事实文案)。
|
|
40
42
|
readonly name: string;
|
|
41
43
|
// 一句话说明这个模式干什么;空串表示没写。
|
|
@@ -76,6 +78,8 @@ export interface SessionMode {
|
|
|
76
78
|
// 这个模式的默认模型;省略就跟全局 `agent-default-model`。**可选**:没配的模式在页面上不出现在这一行
|
|
77
79
|
// (`defaultModel` 是它所在模式的一个可加字段)。
|
|
78
80
|
readonly defaultModel?: SessionModeModel;
|
|
81
|
+
// 退役字段:单个 `preset` 已换成白名单 `presetsOnly`。这里留着只为**报错**(见 `configProblem`),页面上不出现。
|
|
82
|
+
readonly preset?: string;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
// `skills` 不写时的推导:起点是白名单(留空 = 起点是全部工具,含 `skill`),减去黑名单,还留着 `skill` 就要技能
|
|
@@ -121,8 +125,8 @@ const modelSchema = z.object({
|
|
|
121
125
|
),
|
|
122
126
|
reasoningEffort: z.string().description(
|
|
123
127
|
localized({
|
|
124
|
-
zh: "
|
|
125
|
-
en: "Reasoning effort;
|
|
128
|
+
zh: "思考档位;默认跟服务商自己的默认。",
|
|
129
|
+
en: "Reasoning effort; the default is the provider's own.",
|
|
126
130
|
}),
|
|
127
131
|
),
|
|
128
132
|
});
|
|
@@ -149,13 +153,17 @@ export interface ResolvedConfig {
|
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
const modeSchema: z<SessionMode> = z.object({
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
// 退役字段:这里留着只为**报错**(见 `configProblem`),页面上不出现(`hidden()`)。不给默认:没配就没这个键。
|
|
157
|
+
preset: z.string().hidden(),
|
|
158
|
+
presetsOnly: z
|
|
159
|
+
.array(z.string())
|
|
160
|
+
// 候选是部署里注册的 agent preset(具名源 `agent-presets`):字段只说"这是选几个",不关心它从哪来。
|
|
161
|
+
.role("select", { source: "agent-presets" })
|
|
162
|
+
.default([])
|
|
155
163
|
.description(
|
|
156
164
|
localized({
|
|
157
|
-
zh: "
|
|
158
|
-
en: "Which agent
|
|
165
|
+
zh: "这个模式只允许挂哪些 agent preset(它们的 id,官方或本部署自建的):行清单由挂着的那份提供,几个模式可以共享同一份名单。留空表示**允许全部**——哪份 preset 都行,选这个模式也不动会话当前挂着的那份;有值时当前的已经在名单里就不动,否则换成名单里的第一个。模式自己的提示词、工具收口与开关照常生效。",
|
|
166
|
+
en: "Which agent presets this mode may ride on (their ids, shipped or deployment-owned): that preset supplies the row list, and several modes may share one list. Empty means **every preset is allowed** — selecting the mode then leaves the session's current preset alone; when set, the current one stays if it is listed, otherwise the session switches to the first entry. The mode's persona, tool narrowing, and switches apply either way.",
|
|
159
167
|
}),
|
|
160
168
|
),
|
|
161
169
|
name: z
|
|
@@ -188,6 +196,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
188
196
|
persona: personaSchema.default({}),
|
|
189
197
|
allowTools: z
|
|
190
198
|
.array(z.string())
|
|
199
|
+
// 候选是部署里的工具名(具名源 `catalog-tools`,由 client 半注册):字段只说"这是选几个",不关心清单从哪来。
|
|
200
|
+
.role("select", { source: "catalog-tools" })
|
|
191
201
|
.default([])
|
|
192
202
|
.description(
|
|
193
203
|
localized({
|
|
@@ -197,6 +207,7 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
197
207
|
),
|
|
198
208
|
denyTools: z
|
|
199
209
|
.array(z.string())
|
|
210
|
+
.role("select", { source: "catalog-tools" })
|
|
200
211
|
.default([])
|
|
201
212
|
.description(
|
|
202
213
|
localized({
|
|
@@ -206,6 +217,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
206
217
|
),
|
|
207
218
|
allowSkills: z
|
|
208
219
|
.array(z.string())
|
|
220
|
+
// 候选是技能注册表里的名字(具名源 `catalog-skills`)。
|
|
221
|
+
.role("select", { source: "catalog-skills" })
|
|
209
222
|
.default([])
|
|
210
223
|
.description(
|
|
211
224
|
localized({
|
|
@@ -215,6 +228,7 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
215
228
|
),
|
|
216
229
|
denySkills: z
|
|
217
230
|
.array(z.string())
|
|
231
|
+
.role("select", { source: "catalog-skills" })
|
|
218
232
|
.default([])
|
|
219
233
|
.description(
|
|
220
234
|
localized({
|
|
@@ -224,6 +238,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
224
238
|
),
|
|
225
239
|
allowPolicies: z
|
|
226
240
|
.array(z.string())
|
|
241
|
+
// 候选是上游那两条 policy 名(具名源 `policies`)。
|
|
242
|
+
.role("select", { source: "policies" })
|
|
227
243
|
.default([])
|
|
228
244
|
.description(
|
|
229
245
|
localized({
|
|
@@ -233,6 +249,7 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
233
249
|
),
|
|
234
250
|
denyPolicies: z
|
|
235
251
|
.array(z.string())
|
|
252
|
+
.role("select", { source: "policies" })
|
|
236
253
|
.default([])
|
|
237
254
|
.description(
|
|
238
255
|
localized({
|
|
@@ -251,8 +268,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
251
268
|
),
|
|
252
269
|
skills: z.boolean().description(
|
|
253
270
|
localized({
|
|
254
|
-
zh: "是否要技能目录(官方 `skill-catalog`
|
|
255
|
-
en: "Whether the skill catalog applies (the official `skill-catalog` injection).
|
|
271
|
+
zh: "是否要技能目录(官方 `skill-catalog` 的注入)。**默认就按这个模式自己的工具名单推导**:`(allowTools 留空 ? 全部 : allowTools) − denyTools` 里含 `skill` 就要;写 `false` 就丢掉官方 `skill-catalog` 的注入(`skill` 工具的可见性仍归 `allowTools`)。",
|
|
272
|
+
en: "Whether the skill catalog applies (the official `skill-catalog` injection). The default derives it from this mode's own tool lists: it applies when `(empty allowTools ? every tool : allowTools) − denyTools` contains `skill`; `false` drops the official `skill-catalog` injection (`skill` tool visibility still belongs to `allowTools`).",
|
|
256
273
|
}),
|
|
257
274
|
),
|
|
258
275
|
runtimeContext: z
|
|
@@ -266,15 +283,14 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
266
283
|
),
|
|
267
284
|
// 这个模式的默认模型。不标 `volatile`:`modes` 本身就是 volatile,整棵子树都在页面上——再标一层会被
|
|
268
285
|
// schemastery 拒(`validateVolatileSchema` 不许 volatile 套 volatile)。
|
|
269
|
-
defaultModel: modelSchema
|
|
270
|
-
// `default(null)` 是"没配就没有这个键":schemastery 对缺省的对象字段会造一个空对象,那样每个模式都会
|
|
286
|
+
defaultModel: modelSchema // `default(null)` 是"没配就没有这个键":schemastery 对缺省的对象字段会造一个空对象,那样每个模式都会
|
|
271
287
|
// 凭空多出一行;给了 null 反而让它保持缺失(页面按非必填处理,从候选加成)。
|
|
272
288
|
// 类型上放行一次:`null` 在这里只是"没有这个键"的写法,schema 的输入形状不接受它。
|
|
273
289
|
.default(null as unknown as SessionModeModel)
|
|
274
290
|
.description(
|
|
275
291
|
localized({
|
|
276
|
-
zh: "
|
|
277
|
-
en: "Default model for this mode;
|
|
292
|
+
zh: "这个模式的默认模型;不设就跟全局默认模型。只在会话还没有模型事实时接管。",
|
|
293
|
+
en: "Default model for this mode; empty follows the global default. Applies only while a session has no model fact yet.",
|
|
278
294
|
}),
|
|
279
295
|
),
|
|
280
296
|
});
|
|
@@ -316,7 +332,9 @@ interface Validated {
|
|
|
316
332
|
Record<
|
|
317
333
|
string,
|
|
318
334
|
{
|
|
319
|
-
//
|
|
335
|
+
// 留空合法的"不限制";允许共享(差异由会话级收口表达),所以这里不做任何映射唯一性校验。
|
|
336
|
+
readonly presetsOnly?: readonly string[];
|
|
337
|
+
// 退役的单个 preset:还配着值就报错(它已经不再生效)。
|
|
320
338
|
readonly preset?: string;
|
|
321
339
|
// 留空合法:不设收窄(用 preset 的全部工具)。
|
|
322
340
|
readonly allowTools?: readonly string[];
|
|
@@ -369,11 +387,18 @@ export function configProblem(config: Validated): string | undefined {
|
|
|
369
387
|
return `session-mode: mode(s) ${unknownPolicies.join(", ")} name unknown policies; the known ones are ${POLICY_NAMES.join(", ")}`;
|
|
370
388
|
}
|
|
371
389
|
// `allowTools` **留空是合法的**:不设收窄,用这个会话挂着的 preset 的全部工具。
|
|
372
|
-
// `
|
|
373
|
-
//
|
|
390
|
+
// `presetsOnly` **允许留空**(不限制会话挂哪份 preset)**也允许共享**(共享时 preset → 模式的反查交给
|
|
391
|
+
// `SessionModes.modeForPreset`);退役的顶层 `models` 还配着值就报错——别让一份"看着像配过"的配置静静地失效。
|
|
374
392
|
if (Object.keys(config.models ?? {}).length > 0) {
|
|
375
393
|
return "session-mode: `models` has moved into each mode's `defaultModel`; move the entries there and drop the top-level `models`";
|
|
376
394
|
}
|
|
395
|
+
// 退役的单个 `preset`(现在是白名单 `presetsOnly`):旧写法照旧"看着像配过"却不再生效,所以装配期拒绝。
|
|
396
|
+
const retiredPresets = Object.entries(config.modes)
|
|
397
|
+
.filter(([, mode]) => mode.preset !== undefined)
|
|
398
|
+
.map(([id]) => id);
|
|
399
|
+
if (retiredPresets.length > 0) {
|
|
400
|
+
return `session-mode: mode(s) ${retiredPresets.join(", ")} still declare \`preset\`; it has moved to \`presetsOnly\` (the presets that mode may ride on)`;
|
|
401
|
+
}
|
|
377
402
|
// 每个模式自己的默认模型:成对给全(`provider` 与 `model` 都要)。
|
|
378
403
|
const partial = Object.entries(config.modes)
|
|
379
404
|
.filter(([, mode]) => {
|
package/src/rows.ts
CHANGED
|
@@ -22,6 +22,7 @@ function list<T>(values: readonly T[] | undefined): readonly T[] | undefined {
|
|
|
22
22
|
|
|
23
23
|
// 一个模式的源定义 → 行 config(schema 的输入形状:可省的字段就省)。
|
|
24
24
|
function modeConfig(source: ModeSource): Record<string, unknown> {
|
|
25
|
+
const presetsOnly = list(source.presetsOnly);
|
|
25
26
|
const allowTools = list(source.allowTools);
|
|
26
27
|
const denyTools = list(source.denyTools);
|
|
27
28
|
const allowSkills = list(source.allowSkills);
|
|
@@ -29,8 +30,8 @@ function modeConfig(source: ModeSource): Record<string, unknown> {
|
|
|
29
30
|
const allowPolicies = list(source.allowPolicies);
|
|
30
31
|
const denyPolicies = list(source.denyPolicies);
|
|
31
32
|
return {
|
|
32
|
-
// `
|
|
33
|
-
...(
|
|
33
|
+
// `presetsOnly` 是可选的:不写(或写成空数组)就是这个键不进 config,schema 默认空数组(= 不限制)。
|
|
34
|
+
...(presetsOnly === undefined ? {} : { presetsOnly }),
|
|
34
35
|
name: source.name,
|
|
35
36
|
description: source.description,
|
|
36
37
|
...(source.role === undefined ? {} : { role: [...source.role] }),
|
package/src/scope.ts
CHANGED
|
@@ -151,11 +151,10 @@ function narrowCatalog(
|
|
|
151
151
|
warn(`skill catalog lines not recognized; denied skills stay listed: ${missing.join(", ")}`);
|
|
152
152
|
return undefined;
|
|
153
153
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
};
|
|
154
|
+
// **只改正文**,`source.entries` 原样留着:官方 `tool-skill` 用那份结构化名单的 digest 判断"目录变没变"
|
|
155
|
+
// (`catalogHistory` 从会话日志里读它),改了它 digest 就永远对不上,于是每一步都重发一条目录。
|
|
156
|
+
// 收窄是"模型看到什么",不是"官方发布过什么":日志里留的是官方发布的完整名单,模型可见面由正文承担。
|
|
157
|
+
return { ...message, content: [{ ...block, text: lines.join("\n") }] };
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
// `skill` 工具调用的技能名:参数形状是官方那一行的 `{ name: string }`,认不出就不判(那是别人的工具)。
|
package/dist/shared-DX2Giyh3.mjs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/* 新会话屏幕上的模式 chip,紧邻工作区占位:几何与 HeroShell 的 .workspace 对齐,两个读起来是一行。
|
|
2
|
-
标签以省略号收窄,图标与箭头始终贴住剩余文本。 */
|
|
3
|
-
|
|
4
|
-
.menuAnchor {
|
|
5
|
-
min-width: 54px;
|
|
6
|
-
max-width: 100%;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
.seat {
|
|
10
|
-
display: inline-flex;
|
|
11
|
-
align-items: center;
|
|
12
|
-
gap: 4px;
|
|
13
|
-
min-width: 0;
|
|
14
|
-
max-width: min(100%, 240px);
|
|
15
|
-
min-height: 28px;
|
|
16
|
-
padding: 0 8px;
|
|
17
|
-
border: none;
|
|
18
|
-
border-radius: 16px;
|
|
19
|
-
background: transparent;
|
|
20
|
-
color: var(--dsw-alias-label-primary);
|
|
21
|
-
font-size: 13px;
|
|
22
|
-
line-height: 20px;
|
|
23
|
-
font-weight: 500;
|
|
24
|
-
white-space: nowrap;
|
|
25
|
-
cursor: pointer;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.seat:not(:disabled):hover,
|
|
29
|
-
.seat[aria-expanded="true"] {
|
|
30
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.seat:disabled {
|
|
34
|
-
cursor: default;
|
|
35
|
-
color: var(--dsw-alias-label-quaternary);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.seatIcon {
|
|
39
|
-
flex: none;
|
|
40
|
-
color: var(--dsw-alias-label-primary);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.seatLabel {
|
|
44
|
-
min-width: 0;
|
|
45
|
-
overflow: hidden;
|
|
46
|
-
text-overflow: ellipsis;
|
|
47
|
-
white-space: nowrap;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.chevron {
|
|
51
|
-
flex: none;
|
|
52
|
-
color: var(--dsw-alias-label-caption);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* 菜单行是"名字 + 说明":光有 id 说不清这个模式干什么。 */
|
|
56
|
-
.item {
|
|
57
|
-
display: flex;
|
|
58
|
-
flex-direction: column;
|
|
59
|
-
gap: 2px;
|
|
60
|
-
max-width: 280px;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
.itemName {
|
|
64
|
-
font-size: 13px;
|
|
65
|
-
line-height: 20px;
|
|
66
|
-
color: var(--dsw-alias-label-primary);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.itemDesc {
|
|
70
|
-
font-size: 12px;
|
|
71
|
-
line-height: 16px;
|
|
72
|
-
color: var(--dsw-alias-label-caption);
|
|
73
|
-
white-space: normal;
|
|
74
|
-
}
|