@steerable/agent-shell 0.6.52 → 0.6.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/brand.js +1 -1
  2. package/dist/host/pack-assembly.d.ts +6 -16
  3. package/dist/host/pack-assembly.js +3 -4
  4. package/dist/host/runtime.js +2 -0
  5. package/dist/i18n.d.ts +23 -0
  6. package/dist/i18n.js +87 -0
  7. package/dist/image-attachment.d.ts +31 -4
  8. package/dist/image-attachment.js +13 -18
  9. package/dist/json-store.js +3 -6
  10. package/dist/local-backend/portable-bundle.d.ts +16 -2
  11. package/dist/local-backend/portable-bundle.js +29 -16
  12. package/dist/local-backend/portable-service.js +25 -11
  13. package/dist/local-backend/router.js +56 -17
  14. package/dist/local-backend/skills/10-goal/SKILL.md +1 -1
  15. package/dist/local-backend/skills/11-loop/SKILL.md +1 -1
  16. package/dist/local-backend/skills/12-create-skill/SKILL.md +2 -2
  17. package/dist/local-executor.d.ts +22 -0
  18. package/dist/local-executor.js +118 -1
  19. package/dist/local-script-registry.js +2 -2
  20. package/dist/mcp-executor.d.ts +42 -10
  21. package/dist/mcp-executor.js +248 -41
  22. package/dist/mcp-server-registry.d.ts +59 -11
  23. package/dist/mcp-server-registry.js +193 -28
  24. package/dist/product-config.d.ts +24 -14
  25. package/dist/project-registry.d.ts +3 -3
  26. package/dist/project-registry.js +2 -2
  27. package/dist/runtime.d.ts +8 -54
  28. package/dist/runtime.js +15 -129
  29. package/dist/scenario/pack.d.ts +1 -1
  30. package/dist/scenario/registry.d.ts +4 -5
  31. package/dist/scenario/registry.js +2 -10
  32. package/dist/server/start.js +1 -2
  33. package/dist/shell-exec.d.ts +62 -0
  34. package/dist/shell-exec.js +147 -0
  35. package/dist/shell-process.d.ts +3 -0
  36. package/dist/shell-process.js +137 -0
  37. package/dist/sidecar/boot.js +4 -0
  38. package/dist/sidecar/reverse-spawn.js +6 -15
  39. package/dist/sidecar/supervisor.d.ts +0 -2
  40. package/dist/sidecar/supervisor.js +7 -30
  41. package/dist/storage/index.js +1 -1
  42. package/dist/storage/write-lease-error.d.ts +2 -11
  43. package/dist/storage/write-lease-error.js +3 -8
  44. package/dist/tool-router.js +21 -6
  45. package/package.json +7 -14
  46. package/dist/app-update-electron.d.ts +0 -5
  47. package/dist/app-update-electron.js +0 -66
  48. package/dist/app-update.d.ts +0 -92
  49. package/dist/app-update.js +0 -213
  50. package/dist/host/ipc.d.ts +0 -11
  51. package/dist/host/ipc.js +0 -20
  52. package/dist/main.d.ts +0 -1
  53. package/dist/main.js +0 -967
  54. package/dist/preload-default.d.ts +0 -1
  55. package/dist/preload-default.js +0 -9
  56. package/dist/preload.cjs +0 -425
  57. package/dist/preload.d.ts +0 -20
  58. package/dist/preload.js +0 -442
  59. package/dist/single-instance.d.ts +0 -11
  60. package/dist/single-instance.js +0 -21
package/dist/brand.js CHANGED
@@ -34,7 +34,7 @@ const SHELL_BRAND = {
34
34
  displayName: 'Steerable Shell',
35
35
  title: 'Steerable Shell',
36
36
  agentName: 'Agent',
37
- tagline: '一款本地桌面 AI 伙伴',
37
+ tagline: 'A local desktop AI partner',
38
38
  defaultAgentId: LOCAL_ASSISTANT_AGENT_ID,
39
39
  };
40
40
  let productBrand = null;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 包装配注册表(阶段 2.3)——纯构建期组合的宿主侧机制。
3
3
  *
4
- * 宿主(host/runtime.ts、main.ts、server/index.ts)不再静态 import 任何
4
+ * 宿主(host/runtime.ts、server/index.ts)不再静态 import 任何
5
5
  * 包代码:产品组装根(products/<id>/active.ts)把包的装配函数注册进本
6
6
  * 注册表,宿主在装配期逐个调用。产品的 tsc 编译单元只 include 自己的包,
7
7
  * 他包代码物理上不进产品 dist。
@@ -10,11 +10,10 @@
10
10
  * - import 期(active.ts 顶层):迁移/种子/品牌等无宿主依赖的注册;
11
11
  * - 装配期(createHostRuntime 内):服务/工具/路由等需要宿主能力的部分,
12
12
  * 经 {@link PackAssemblyDeps} 注入。
13
- * 入口差异:CS(main.ts)额外消费 handle.ipc,BS(server/index.ts)额外
14
- * 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,宿主代码里
15
- * 不出现任何包名。
13
+ * server/index.ts 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,
14
+ * 宿主代码里不出现任何包名。
16
15
  */
17
- import type { HttpRouteContribution, IpcContribution, ToolContribution } from '../scenario/pack.js';
16
+ import type { HttpRouteContribution, ToolContribution } from '../scenario/pack.js';
18
17
  import type { PackDbAccess } from '../storage/driver.js';
19
18
  /** 宿主注入包的装配能力面。包不 import 宿主装配模块,全部经回调注入。 */
20
19
  export interface PackAssemblyDeps {
@@ -31,7 +30,7 @@ export interface PackAssemblyDeps {
31
30
  name: string;
32
31
  folderPath: string;
33
32
  } | null>;
34
- /** 面向全部用户面的事件广播(CS=所有窗口 IPC,BS=SSE 总线)。 */
33
+ /** 面向全部用户面的事件广播(SSE 总线)。 */
35
34
  readonly broadcast: (channel: string, payload: unknown) => void;
36
35
  /** 工具贡献注册口(toolRouter.registerToolContributions 的收窄面)。 */
37
36
  readonly registerTools: (tools: readonly ToolContribution[]) => void;
@@ -61,20 +60,11 @@ export interface PackAssemblyDeps {
61
60
  /** 装配期日志行(进宿主日志)。 */
62
61
  readonly onLog: (line: string) => void;
63
62
  }
64
- /** CS 宿主的窗口能力:打开一个加载应用内路由的独立窗口。 */
65
- export interface PackHostWindowCapabilities {
66
- /** route 如 '/<pack>-debug-log';title 为窗口副标题(宿主拼品牌名)。 */
67
- openRouteWindow(route: string, options: {
68
- title: string;
69
- }): Promise<void>;
70
- }
71
63
  /** 装配产物:宿主按入口面消费。全部槽位可选。 */
72
64
  export interface PackAssemblyHandle {
73
65
  /** 宿主关停钩子(进程退出时按装配逆序调用)。 */
74
66
  readonly dispose?: () => void | Promise<void>;
75
- /** CS:包 IPC 贡献(需要宿主窗口能力时经 caps 注入)。 */
76
- readonly ipc?: (caps: PackHostWindowCapabilities) => readonly IpcContribution[];
77
- /** BS:/host/<packId>/* 路由贡献。 */
67
+ /** /host/<packId>/* 路由贡献。 */
78
68
  readonly httpRoutes?: () => readonly HttpRouteContribution[];
79
69
  }
80
70
  export type PackAssembly = (deps: PackAssemblyDeps) => PackAssemblyHandle | void;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 包装配注册表(阶段 2.3)——纯构建期组合的宿主侧机制。
3
3
  *
4
- * 宿主(host/runtime.ts、main.ts、server/index.ts)不再静态 import 任何
4
+ * 宿主(host/runtime.ts、server/index.ts)不再静态 import 任何
5
5
  * 包代码:产品组装根(products/<id>/active.ts)把包的装配函数注册进本
6
6
  * 注册表,宿主在装配期逐个调用。产品的 tsc 编译单元只 include 自己的包,
7
7
  * 他包代码物理上不进产品 dist。
@@ -10,9 +10,8 @@
10
10
  * - import 期(active.ts 顶层):迁移/种子/品牌等无宿主依赖的注册;
11
11
  * - 装配期(createHostRuntime 内):服务/工具/路由等需要宿主能力的部分,
12
12
  * 经 {@link PackAssemblyDeps} 注入。
13
- * 入口差异:CS(main.ts)额外消费 handle.ipc,BS(server/index.ts)额外
14
- * 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,宿主代码里
15
- * 不出现任何包名。
13
+ * server/index.ts 消费 handle.httpRoutes——包的入口面贡献由宿主循环驱动,
14
+ * 宿主代码里不出现任何包名。
16
15
  */
17
16
  const assemblies = new Map();
18
17
  /** 注册包的装配函数(产品组装根在 import 期调用;重复注册同包抛错)。 */
@@ -16,6 +16,7 @@ import { LocalScriptRegistry } from '../local-script-registry.js';
16
16
  import { TerminalManager } from '../terminal-manager.js';
17
17
  import { ToolRouter } from '../tool-router.js';
18
18
  import { McpServerRegistry, } from '../mcp-server-registry.js';
19
+ import { mcpExecutor } from '../mcp-executor.js';
19
20
  import { ProjectRegistry } from '../project-registry.js';
20
21
  import { getProductConfig } from '../product-config.js';
21
22
  import { LocalBackendRouter } from '../local-backend/router.js';
@@ -264,6 +265,7 @@ export async function createHostRuntime(options) {
264
265
  for (const handle of [...packHandles.values()].reverse()) {
265
266
  await handle.dispose?.();
266
267
  }
268
+ await mcpExecutor.shutdownAll();
267
269
  await shutdownHostSidecar();
268
270
  await closeStorage();
269
271
  },
package/dist/i18n.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 主进程界面文案。与渲染层同一规则:英文是源句,应用层 registerLocale 注入译文。
3
+ * 本模块不依赖 Electron,菜单在 app ready 时读取。
4
+ */
5
+ export declare const SOURCE_LOCALE = "en";
6
+ export declare function normalizeLocale(raw: string | null | undefined): string | null;
7
+ export declare function resolveLocale(options: {
8
+ supported: readonly string[];
9
+ defaultLocale: string;
10
+ saved: string | null;
11
+ system: string | null;
12
+ }): string;
13
+ export declare function registerLocale(locale: string, messages: Record<string, string>): void;
14
+ export declare function configureI18n(options: {
15
+ locales: readonly string[];
16
+ defaultLocale: string;
17
+ systemLocale?: string | null;
18
+ savedLocale?: string | null;
19
+ }): void;
20
+ export declare function getLocale(): string;
21
+ export declare function setLocale(locale: string): boolean;
22
+ export declare function t(message: string, params?: Record<string, string | number>): string;
23
+ export declare function resetI18nForTests(): void;
package/dist/i18n.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * 主进程界面文案。与渲染层同一规则:英文是源句,应用层 registerLocale 注入译文。
3
+ * 本模块不依赖 Electron,菜单在 app ready 时读取。
4
+ */
5
+ export const SOURCE_LOCALE = 'en';
6
+ export function normalizeLocale(raw) {
7
+ if (!raw)
8
+ return null;
9
+ const lower = raw.trim().toLowerCase().replace(/_/g, '-');
10
+ if (!lower)
11
+ return null;
12
+ if (lower.startsWith('zh'))
13
+ return 'zh';
14
+ if (lower.startsWith('en'))
15
+ return 'en';
16
+ const primary = lower.split('-')[0];
17
+ return primary || null;
18
+ }
19
+ export function resolveLocale(options) {
20
+ const supported = options.supported.filter((locale) => locale.length > 0);
21
+ const list = supported.length > 0 ? supported : [SOURCE_LOCALE];
22
+ const pick = (value) => {
23
+ if (!value)
24
+ return null;
25
+ if (list.includes(value))
26
+ return value;
27
+ const normalized = normalizeLocale(value);
28
+ if (normalized && list.includes(normalized))
29
+ return normalized;
30
+ return null;
31
+ };
32
+ return (pick(options.saved) ??
33
+ pick(options.system) ??
34
+ pick(options.defaultLocale) ??
35
+ list[0] ??
36
+ SOURCE_LOCALE);
37
+ }
38
+ function formatMessage(template, params) {
39
+ if (!params)
40
+ return template;
41
+ return template.replace(/\{(\w+)\}/g, (whole, name) => {
42
+ const value = params[name];
43
+ return value === undefined ? whole : String(value);
44
+ });
45
+ }
46
+ const catalogs = {};
47
+ let locales = [SOURCE_LOCALE];
48
+ let defaultLocale = SOURCE_LOCALE;
49
+ let current = SOURCE_LOCALE;
50
+ export function registerLocale(locale, messages) {
51
+ const normalized = locale.trim();
52
+ if (!normalized || normalized === SOURCE_LOCALE)
53
+ return;
54
+ catalogs[normalized] = { ...catalogs[normalized], ...messages };
55
+ }
56
+ export function configureI18n(options) {
57
+ const next = [...new Set(options.locales.filter((locale) => locale.length > 0))];
58
+ locales = next.length > 0 ? next : [SOURCE_LOCALE];
59
+ defaultLocale = options.defaultLocale || SOURCE_LOCALE;
60
+ current = resolveLocale({
61
+ supported: locales,
62
+ defaultLocale,
63
+ saved: options.savedLocale ?? null,
64
+ system: options.systemLocale ?? null,
65
+ });
66
+ }
67
+ export function getLocale() {
68
+ return current;
69
+ }
70
+ export function setLocale(locale) {
71
+ if (!locales.includes(locale))
72
+ return false;
73
+ current = locale;
74
+ return true;
75
+ }
76
+ export function t(message, params) {
77
+ const table = current === SOURCE_LOCALE ? undefined : catalogs[current];
78
+ const template = table?.[message] ?? message;
79
+ return formatMessage(template, params);
80
+ }
81
+ export function resetI18nForTests() {
82
+ for (const key of Object.keys(catalogs))
83
+ delete catalogs[key];
84
+ locales = [SOURCE_LOCALE];
85
+ defaultLocale = SOURCE_LOCALE;
86
+ current = SOURCE_LOCALE;
87
+ }
@@ -1,4 +1,3 @@
1
- import type { NativeImageLike } from './runtime.js';
2
1
  import type { LlmImage } from './llm/types.js';
3
2
  /** Refuse to read source files larger than this (10 MB). */
4
3
  export declare const IMAGE_MAX_SOURCE_BYTES: number;
@@ -6,6 +5,34 @@ export declare const IMAGE_MAX_SOURCE_BYTES: number;
6
5
  export declare const IMAGE_MAX_DIMENSION = 1568;
7
6
  /** Refuse the encoded payload if it still exceeds this after re-encode (5 MB). */
8
7
  export declare const IMAGE_MAX_ENCODED_BYTES: number;
8
+ /** A decoded raster that can be cropped, resized, and re-encoded synchronously. */
9
+ export interface DecodedImage {
10
+ isEmpty(): boolean;
11
+ getSize(): {
12
+ width: number;
13
+ height: number;
14
+ };
15
+ crop(rect: {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ }): DecodedImage;
21
+ resize(o: {
22
+ width?: number;
23
+ height?: number;
24
+ quality?: string;
25
+ }): DecodedImage;
26
+ toPNG(): Buffer;
27
+ toJPEG(quality: number): Buffer;
28
+ }
29
+ /**
30
+ * Optional image decoder. Without one, images pass through as the original
31
+ * bytes and crop / resize / transcode requests are refused.
32
+ */
33
+ export interface ImageDecoder {
34
+ createFromPath(p: string): DecodedImage;
35
+ }
9
36
  export interface ImageAttachmentInput {
10
37
  path: string;
11
38
  name?: string;
@@ -65,9 +92,9 @@ export declare function computeTargetSize(width: number, height: number, maxDime
65
92
  * tool result. `_image` is consumed by the CoreLoop as an image content part;
66
93
  * it is not a path or model-visible base64 string.
67
94
  */
68
- export declare function processViewImage(input: ViewImageInput, nativeImage?: NativeImageLike | null): ViewImageResult;
95
+ export declare function processViewImage(input: ViewImageInput, decoder?: ImageDecoder | null): ViewImageResult;
69
96
  /**
70
- * Process a batch of image attachments. Synchronous: `nativeImage` decode /
97
+ * Process a batch of image attachments. Synchronous: decoder decode /
71
98
  * resize / encode are all synchronous, and the byte check uses `statSync`.
72
99
  */
73
- export declare function processImageAttachments(files: ImageAttachmentInput[]): ProcessedImageAttachments;
100
+ export declare function processImageAttachments(files: ImageAttachmentInput[], decoder?: ImageDecoder | null): ProcessedImageAttachments;
@@ -3,9 +3,9 @@
3
3
  * actually see, replacing the old behavior of dropping only the file *path*
4
4
  * into the prompt text.
5
5
  *
6
- * Runs in the Electron main process and uses `nativeImage` to decode /
7
- * downscale — no native dependency, works in the packaged app. Two caps are
8
- * enforced before the image ever reaches the model:
6
+ * Decoding / downscaling runs only when an {@link ImageDecoder} is supplied;
7
+ * otherwise the original bytes pass through under the encoded cap. These caps
8
+ * are enforced before the image ever reaches the model:
9
9
  *
10
10
  * - source bytes (`IMAGE_MAX_SOURCE_BYTES`) — refuse to read huge files;
11
11
  * - long-edge px (`IMAGE_MAX_DIMENSION`) — downscale so the provider's
@@ -17,7 +17,6 @@
17
17
  * line so the caller can inject it into the model-visible context — the model
18
18
  * should know an image was attached even when it was too large to send.
19
19
  */
20
- import { getNativeImage } from './runtime.js';
21
20
  import { readFileSync, statSync } from 'fs';
22
21
  import { basename, extname } from 'path';
23
22
  /** Refuse to read source files larger than this (10 MB). */
@@ -74,7 +73,7 @@ export function computeTargetSize(width, height, maxDimension = IMAGE_MAX_DIMENS
74
73
  * tool result. `_image` is consumed by the CoreLoop as an image content part;
75
74
  * it is not a path or model-visible base64 string.
76
75
  */
77
- export function processViewImage(input, nativeImage = getNativeImage()) {
76
+ export function processViewImage(input, decoder = null) {
78
77
  if (!isImagePath(input.path)) {
79
78
  return {
80
79
  success: false,
@@ -104,10 +103,10 @@ export function processViewImage(input, nativeImage = getNativeImage()) {
104
103
  needsFollowup: true,
105
104
  };
106
105
  }
107
- if (!nativeImage) {
106
+ if (!decoder) {
108
107
  return viewImageWithoutDecoder(input, sourceBytes, maxEdge);
109
108
  }
110
- let rendered = nativeImage.createFromPath(input.path);
109
+ let rendered = decoder.createFromPath(input.path);
111
110
  if (rendered.isEmpty()) {
112
111
  return { success: false, error: '不是可识别的图片', needsFollowup: true };
113
112
  }
@@ -262,16 +261,15 @@ function formatMb(bytes) {
262
261
  return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '');
263
262
  }
264
263
  /**
265
- * Process a batch of image attachments. Synchronous: `nativeImage` decode /
264
+ * Process a batch of image attachments. Synchronous: decoder decode /
266
265
  * resize / encode are all synchronous, and the byte check uses `statSync`.
267
266
  */
268
- export function processImageAttachments(files) {
267
+ export function processImageAttachments(files, decoder = null) {
269
268
  const images = [];
270
269
  const notes = [];
271
270
  for (const file of files) {
272
271
  const label = file.name || basename(file.path);
273
- // Existence + source-size guards run before the decoder so they're
274
- // exercisable in a non-Electron (test) host too.
272
+ // Existence + source-size guards run before the decoder.
275
273
  let sourceBytes = 0;
276
274
  try {
277
275
  sourceBytes = statSync(file.path).size;
@@ -284,12 +282,9 @@ export function processImageAttachments(files) {
284
282
  notes.push(`- ${label}:源文件 ${formatMb(sourceBytes)}MB 超过 ${formatMb(IMAGE_MAX_SOURCE_BYTES)}MB 上限,未附加`);
285
283
  continue;
286
284
  }
287
- const nativeImage = getNativeImage();
288
- if (!nativeImage) {
289
- // Tauri and the browser server run plain Node, which has no Electron
290
- // nativeImage. The bytes are already a PNG/JPEG the provider can read;
291
- // pass them through when they fit the encoded cap. Resize stays on the
292
- // Electron path.
285
+ if (!decoder) {
286
+ // The bytes are already an image the provider can read; pass them
287
+ // through when they fit the encoded cap.
293
288
  if (sourceBytes > IMAGE_MAX_ENCODED_BYTES) {
294
289
  notes.push(`- ${label}:源文件 ${formatMb(sourceBytes)}MB 超过 ${formatMb(IMAGE_MAX_ENCODED_BYTES)}MB,当前环境不能缩放,未附加`);
295
290
  continue;
@@ -307,7 +302,7 @@ export function processImageAttachments(files) {
307
302
  notes.push(`- ${label}(原图)`);
308
303
  continue;
309
304
  }
310
- const image = nativeImage.createFromPath(file.path);
305
+ const image = decoder.createFromPath(file.path);
311
306
  if (image.isEmpty()) {
312
307
  notes.push(`- ${label}:不是可识别的图片,未附加`);
313
308
  continue;
@@ -1,10 +1,7 @@
1
1
  /**
2
- * JSON 键值存储工厂(agent-mcp-servers.json / agent-projects.json / window-state)。
3
- *
4
- * 历史上用 electron-store;它只在构造时读 `app.getPath('userData')` 当 cwd,
5
- * 而 conf(electron-store 的底层,已是本项目直接依赖)API 完全覆盖用到的
6
- * get/set + name + defaults 子集。统一走 conf + getUserDataDir(),Electron
7
- * 与 BS server 得到同一个文件路径,两种模式共享同一份数据。
2
+ * JSON 键值存储工厂(agent-mcp-servers.json / agent-projects.json /
3
+ * agent-local-scripts.json)。文件在 getUserDataDir() 下,由 conf 读写;
4
+ * 桌面与 BS server 得到同一个文件路径。
8
5
  *
9
6
  * conf@10 的 d.ts 是 ESM 风格但包本身没有 `type: module`,NodeNext 下类型
10
7
  * 解析会塌成 namespace——所以这里声明用到的最小接口并做一次强转,不依赖
@@ -69,9 +69,14 @@ export interface PortableWebSearchSection {
69
69
  }
70
70
  export interface PortableMcpSection {
71
71
  name: string;
72
- command: string;
73
- args: string[];
72
+ transport: 'stdio' | 'streamable-http';
73
+ command?: string;
74
+ args?: string[];
74
75
  cwd?: string;
76
+ url?: string;
77
+ headers?: Record<string, string>;
78
+ headersFromEnv?: Record<string, string>;
79
+ bearerTokenEnvVar?: string;
75
80
  enabled: boolean;
76
81
  envIncluded: boolean;
77
82
  env?: Record<string, string>;
@@ -178,11 +183,20 @@ export declare function webSearchSectionFromSettings(settings: {
178
183
  }, includeSecrets: boolean): PortableWebSearchSection;
179
184
  export declare function mcpSectionFromServer(server: {
180
185
  name: string;
186
+ transport: 'stdio';
181
187
  command: string;
182
188
  args: string[];
183
189
  cwd?: string;
184
190
  enabled: boolean;
185
191
  env: Record<string, string>;
192
+ } | {
193
+ name: string;
194
+ transport: 'streamable-http';
195
+ url: string;
196
+ headers: Record<string, string>;
197
+ headersFromEnv: Record<string, string>;
198
+ bearerTokenEnvVar?: string;
199
+ enabled: boolean;
186
200
  }, includeSecrets: boolean): PortableMcpSection;
187
201
  /**
188
202
  * 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
@@ -5,6 +5,7 @@
5
5
  * 钥匙(模型 Key、搜索 Key、MCP 环境变量)默认不进包;`includeSecrets`
6
6
  * 为 true 时原样写入,导入方要自己确认。
7
7
  */
8
+ import { t } from '../i18n.js';
8
9
  export const PORTABLE_SCHEMA_VERSION = 1;
9
10
  export const PORTABLE_MESSAGE_LIMIT = 1000;
10
11
  export const PORTABLE_CONFIG_KIND = 'steerable-config';
@@ -64,19 +65,19 @@ export function readPortableProject(raw) {
64
65
  }
65
66
  function thinkingLabel(mode) {
66
67
  if (mode === 'hidden')
67
- return '隐藏';
68
+ return t('Hide');
68
69
  if (mode === 'full')
69
- return '完整显示';
70
+ return t('Show all');
70
71
  if (mode === 'peek')
71
- return '显示5行';
72
- return '未识别的显示档位';
72
+ return t('Show 5 lines');
73
+ return t('Unrecognized display mode');
73
74
  }
74
75
  function execLabel(policy) {
75
76
  if (policy === 'full')
76
- return '完整权限';
77
+ return t('Full access');
77
78
  if (policy === 'workspace')
78
- return '工作区';
79
- return '未识别的权限';
79
+ return t('Workspace');
80
+ return t('Unrecognized permission');
80
81
  }
81
82
  export function llmSectionFromSettings(settings, includeSecrets) {
82
83
  const apiKey = asString(settings.apiKey);
@@ -106,15 +107,27 @@ export function webSearchSectionFromSettings(settings, includeSecrets) {
106
107
  };
107
108
  }
108
109
  export function mcpSectionFromServer(server, includeSecrets) {
109
- return {
110
- name: server.name,
111
- command: server.command,
112
- args: [...server.args],
113
- cwd: server.cwd,
114
- enabled: server.enabled,
115
- envIncluded: includeSecrets,
116
- ...(includeSecrets ? { env: { ...server.env } } : {}),
117
- };
110
+ return server.transport === 'stdio'
111
+ ? {
112
+ name: server.name,
113
+ transport: 'stdio',
114
+ command: server.command,
115
+ args: [...server.args],
116
+ cwd: server.cwd,
117
+ enabled: server.enabled,
118
+ envIncluded: includeSecrets,
119
+ ...(includeSecrets ? { env: { ...server.env } } : {}),
120
+ }
121
+ : {
122
+ name: server.name,
123
+ transport: 'streamable-http',
124
+ url: server.url,
125
+ headers: { ...server.headers },
126
+ headersFromEnv: { ...server.headersFromEnv },
127
+ bearerTokenEnvVar: server.bearerTokenEnvVar,
128
+ enabled: server.enabled,
129
+ envIncluded: false,
130
+ };
118
131
  }
119
132
  /**
120
133
  * 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
@@ -180,22 +180,36 @@ async function applyMcp(mcp, servers) {
180
180
  return 'MCP 注册表不可用';
181
181
  for (const server of servers) {
182
182
  const name = server.name?.trim();
183
- const command = server.command?.trim();
184
- if (!name || !command)
183
+ if (!name)
185
184
  continue;
186
- const input = {
187
- name,
188
- command,
189
- args: Array.isArray(server.args) ? server.args.map((arg) => String(arg)) : [],
190
- cwd: server.cwd,
191
- enabled: server.enabled !== false,
192
- ...(server.envIncluded && server.env ? { env: server.env } : {}),
193
- };
185
+ const input = server.transport === 'streamable-http'
186
+ ? {
187
+ name,
188
+ transport: 'streamable-http',
189
+ url: server.url?.trim() ?? '',
190
+ headers: server.headers ?? {},
191
+ headersFromEnv: server.headersFromEnv ?? {},
192
+ bearerTokenEnvVar: server.bearerTokenEnvVar,
193
+ enabled: server.enabled !== false,
194
+ }
195
+ : {
196
+ name,
197
+ transport: 'stdio',
198
+ command: server.command?.trim() ?? '',
199
+ args: Array.isArray(server.args) ? server.args.map((arg) => String(arg)) : [],
200
+ cwd: server.cwd,
201
+ enabled: server.enabled !== false,
202
+ ...(server.envIncluded && server.env ? { env: server.env } : {}),
203
+ };
204
+ if ((input.transport === 'stdio' && !input.command) ||
205
+ (input.transport === 'streamable-http' && !input.url)) {
206
+ continue;
207
+ }
194
208
  const existing = mcp.list().find((item) => item.name.toLowerCase() === name.toLowerCase());
195
209
  if (existing)
196
210
  mcp.update(existing.id, input);
197
211
  else
198
- mcp.create({ ...input, env: input.env ?? {} });
212
+ mcp.create(input);
199
213
  }
200
214
  return null;
201
215
  }