@steerable/agent-shell 0.6.48 → 0.6.50

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.
@@ -208,9 +208,9 @@ export async function createHostRuntime(options) {
208
208
  onLog(`[task] swept ${sweptTasks} stale running task(s)`);
209
209
  }
210
210
  void localExecutor.init();
211
- // Not awaited: sidecar boot (python import) must not delay readiness.
212
- // A turn that races the boot falls back for that turn only.
213
- void startHostSidecar({
211
+ // The complete Python sidecar is mandatory. Do not report host
212
+ // readiness or fall back to an in-process loop when it cannot start.
213
+ await startHostSidecar({
214
214
  store: defaultStore,
215
215
  toolRouter,
216
216
  resolveProjectRoot: async (chatId) => localBackendRouter.resolveChatWorkspaceRoot(chatId),
@@ -34,7 +34,7 @@ export type ResolvedSettingsChrome = Record<SettingsItemId, boolean>;
34
34
  */
35
35
  export declare function resolveSettingsChrome(value?: unknown, tools?: ResolvedHostTools): ResolvedSettingsChrome;
36
36
  export declare function hasGeneralSettingsChrome(chrome?: ResolvedSettingsChrome): boolean;
37
- export declare const LOCAL_FS_TOOL_NAMES: readonly ["local_exec_shell", "local_read_file", "view_image", "local_write_file", "local_edit_file", "local_open_path", "local_run_snippet", "local_list_scripts", "local_run_script"];
37
+ export declare const LOCAL_FS_TOOL_NAMES: readonly ["local_exec_shell", "local_read_file", "view_image", "local_write_file", "local_edit_file", "present_files", "local_open_path", "local_run_snippet", "local_list_scripts", "local_run_script"];
38
38
  export declare function resolveHostToolSurface(value: boolean | {
39
39
  capability?: boolean;
40
40
  chrome?: boolean;
@@ -79,6 +79,7 @@ export const LOCAL_FS_TOOL_NAMES = [
79
79
  'view_image',
80
80
  'local_write_file',
81
81
  'local_edit_file',
82
+ 'present_files',
82
83
  'local_open_path',
83
84
  'local_run_snippet',
84
85
  'local_list_scripts',
@@ -2,10 +2,6 @@
2
2
  * `LlmProvider` implementation that delegates to the steerable-sidecar's
3
3
  * `agent.chat.stream` JSON-RPC method.
4
4
  *
5
- * Activation:
6
- * Default-on once the sidecar is supervised (see `llm/index.ts`);
7
- * `STEERABLE_USE_SIDECAR=0` opts back out to in-process providers.
8
- *
9
5
  * The sidecar must already be supervised by the main process (see
10
6
  * `src/sidecar/supervisor.ts`); this provider just routes calls through the
11
7
  * supplied supervisor handle and aggregates streamed chunks back into the
@@ -2,10 +2,6 @@
2
2
  * `LlmProvider` implementation that delegates to the steerable-sidecar's
3
3
  * `agent.chat.stream` JSON-RPC method.
4
4
  *
5
- * Activation:
6
- * Default-on once the sidecar is supervised (see `llm/index.ts`);
7
- * `STEERABLE_USE_SIDECAR=0` opts back out to in-process providers.
8
- *
9
5
  * The sidecar must already be supervised by the main process (see
10
6
  * `src/sidecar/supervisor.ts`); this provider just routes calls through the
11
7
  * supplied supervisor handle and aggregates streamed chunks back into the
@@ -9,9 +9,7 @@
9
9
  *
10
10
  * This is the only chat path (the TS loop was deleted 2026-08-26). If the
11
11
  * sidecar failed to start, the router fails loud with a 503 — there is no
12
- * in-process fallback loop anymore. `STEERABLE_USE_SIDECAR=0` still
13
- * disables the sidecar process itself (auxiliary LLM calls then fall back
14
- * to the in-proc providers), but chat requires the sidecar.
12
+ * in-process fallback loop or sidecar opt-out.
15
13
  */
16
14
  import type { SidecarChatStreamRequest, SidecarChildEvent, SidecarRawChunk, SidecarSupervisor } from '../sidecar/index.js';
17
15
  import type { LlmMessage } from '../llm/types.js';
@@ -9,9 +9,7 @@
9
9
  *
10
10
  * This is the only chat path (the TS loop was deleted 2026-08-26). If the
11
11
  * sidecar failed to start, the router fails loud with a 503 — there is no
12
- * in-process fallback loop anymore. `STEERABLE_USE_SIDECAR=0` still
13
- * disables the sidecar process itself (auxiliary LLM calls then fall back
14
- * to the in-proc providers), but chat requires the sidecar.
12
+ * in-process fallback loop or sidecar opt-out.
15
13
  */
16
14
  /**
17
15
  * Active CoreLoop turns by chatId — lets the main process route a mid-turn
@@ -0,0 +1,202 @@
1
+ /**
2
+ * 对话包 / 配置包的文件格式。纯函数:不读库、不写盘。
3
+ *
4
+ * 文件是一个 JSON 对象。`kind` 区分配置、单条对话和多条对话,`schemaVersion` 只认 1。
5
+ * 钥匙(模型 Key、搜索 Key、MCP 环境变量)默认不进包;`includeSecrets`
6
+ * 为 true 时原样写入,导入方要自己确认。
7
+ */
8
+ export declare const PORTABLE_SCHEMA_VERSION = 1;
9
+ export declare const PORTABLE_MESSAGE_LIMIT = 1000;
10
+ export declare const PORTABLE_CONFIG_KIND = "steerable-config";
11
+ export declare const PORTABLE_CHAT_KIND = "steerable-chat";
12
+ export declare const PORTABLE_CHATS_KIND = "steerable-chats";
13
+ export declare const SERVER_SECTION_IDS: readonly ["llm", "webSearch", "telemetry", "insights", "mcp", "agents", "skills"];
14
+ export declare const CLIENT_SECTION_IDS: readonly ["appearance", "execPolicy"];
15
+ export type ServerSectionId = (typeof SERVER_SECTION_IDS)[number];
16
+ export type ClientSectionId = (typeof CLIENT_SECTION_IDS)[number];
17
+ export type PortableSectionId = ServerSectionId | ClientSectionId;
18
+ export interface PortablePreviewSection {
19
+ id: PortableSectionId;
20
+ label: string;
21
+ detail: string;
22
+ clientOnly: boolean;
23
+ hasSecret: boolean;
24
+ }
25
+ export interface PortableChatSummary {
26
+ title: string;
27
+ messageCount: number;
28
+ attachmentCount: number;
29
+ omittedAttachmentCount: number;
30
+ truncated: boolean;
31
+ /** 多条对话包里的会话数。单条对话包不带这个字段。 */
32
+ count?: number;
33
+ /** 单条对话所属项目,或多条包里只有一个项目时的名字。 */
34
+ projectName?: string;
35
+ /** 包里出现的项目数。没有项目时不带。 */
36
+ projectCount?: number;
37
+ }
38
+ /** 项目登记。只含名称和目录位置,不含目录里的文件。 */
39
+ export interface PortableProject {
40
+ name: string;
41
+ folderPath: string;
42
+ sourceFolders: string[];
43
+ trusted: boolean;
44
+ }
45
+ export interface PortablePreview {
46
+ kind: typeof PORTABLE_CONFIG_KIND | typeof PORTABLE_CHAT_KIND | typeof PORTABLE_CHATS_KIND;
47
+ includeSecrets: boolean;
48
+ sections: PortablePreviewSection[];
49
+ chat?: PortableChatSummary;
50
+ }
51
+ export interface PortableLlmSection {
52
+ provider?: string;
53
+ vendorId?: string;
54
+ model?: string;
55
+ baseUrl?: string;
56
+ temperature?: number;
57
+ systemPrompt?: string;
58
+ maxTotalTokens?: number;
59
+ compat?: unknown;
60
+ presets?: unknown;
61
+ execTimeoutSeconds?: number;
62
+ apiKeyIncluded: boolean;
63
+ apiKey?: string;
64
+ }
65
+ export interface PortableWebSearchSection {
66
+ provider?: string;
67
+ apiKeyIncluded: boolean;
68
+ apiKey?: string;
69
+ }
70
+ export interface PortableMcpSection {
71
+ name: string;
72
+ command: string;
73
+ args: string[];
74
+ cwd?: string;
75
+ enabled: boolean;
76
+ envIncluded: boolean;
77
+ env?: Record<string, string>;
78
+ }
79
+ export interface PortableAgentSection {
80
+ name: string;
81
+ slug?: string | null;
82
+ icon?: string | null;
83
+ color?: string | null;
84
+ description?: string | null;
85
+ rolePrompt?: string | null;
86
+ forbiddenPrompt?: string | null;
87
+ skillIds?: string[];
88
+ toolPolicy?: unknown;
89
+ allowExternalSkills?: boolean;
90
+ loadAllSkills?: boolean;
91
+ isArchived?: boolean;
92
+ sortOrder?: number;
93
+ }
94
+ export interface PortableSkillSection {
95
+ names: string[];
96
+ }
97
+ export interface PortableInsightsSection {
98
+ shareBehavior?: boolean;
99
+ shareConversation?: boolean;
100
+ shareProfile?: boolean;
101
+ promptedAt?: string;
102
+ apiBase?: string;
103
+ profile?: {
104
+ displayName?: string;
105
+ email?: string;
106
+ company?: string;
107
+ note?: string;
108
+ };
109
+ }
110
+ export interface PortableTelemetrySection {
111
+ endpoint?: string;
112
+ privacyMode?: string;
113
+ serviceName?: string;
114
+ }
115
+ export interface PortableAppearanceSection {
116
+ thinkingDisplay?: string;
117
+ }
118
+ export interface PortableExecPolicySection {
119
+ policy?: string;
120
+ }
121
+ export interface PortableConfigSections {
122
+ llm?: PortableLlmSection;
123
+ webSearch?: PortableWebSearchSection;
124
+ telemetry?: PortableTelemetrySection;
125
+ insights?: PortableInsightsSection;
126
+ mcp?: PortableMcpSection[];
127
+ agents?: PortableAgentSection[];
128
+ skills?: PortableSkillSection;
129
+ appearance?: PortableAppearanceSection;
130
+ execPolicy?: PortableExecPolicySection;
131
+ }
132
+ export interface PortableConfigDocument {
133
+ kind: typeof PORTABLE_CONFIG_KIND;
134
+ schemaVersion: number;
135
+ exportedAt: string;
136
+ includeSecrets: boolean;
137
+ sections: PortableConfigSections;
138
+ }
139
+ export interface PortableChatMessage {
140
+ role: string;
141
+ content: string;
142
+ messageMetadata: string | null;
143
+ createdAt: string;
144
+ }
145
+ export interface PortableChatAttachment {
146
+ name: string;
147
+ size: number;
148
+ /** base64。过大时缺省,并标 `omitted`。 */
149
+ data?: string;
150
+ omitted?: boolean;
151
+ }
152
+ export interface PortableChatDocument {
153
+ kind: typeof PORTABLE_CHAT_KIND;
154
+ schemaVersion: number;
155
+ exportedAt: string;
156
+ chat: {
157
+ title: string;
158
+ agentId: string | null;
159
+ isPinned: boolean;
160
+ systemPrompt: string | null;
161
+ pinnedRefs: unknown;
162
+ };
163
+ /** 所属项目。没有项目时缺省。目录里的文件不在这里。 */
164
+ project?: PortableProject;
165
+ messages: PortableChatMessage[];
166
+ truncated: boolean;
167
+ attachments: PortableChatAttachment[];
168
+ }
169
+ export type PortableDocument = PortableConfigDocument | PortableChatDocument;
170
+ export declare function isServerSectionId(value: string): value is ServerSectionId;
171
+ export declare function isClientSectionId(value: string): value is ClientSectionId;
172
+ /** 读项目登记。缺名字或缺家目录时当作没有项目,不把目录内容读进来。 */
173
+ export declare function readPortableProject(raw: unknown): PortableProject | null;
174
+ export declare function llmSectionFromSettings(settings: Record<string, unknown>, includeSecrets: boolean): PortableLlmSection;
175
+ export declare function webSearchSectionFromSettings(settings: {
176
+ provider?: string;
177
+ apiKey?: string | null;
178
+ }, includeSecrets: boolean): PortableWebSearchSection;
179
+ export declare function mcpSectionFromServer(server: {
180
+ name: string;
181
+ command: string;
182
+ args: string[];
183
+ cwd?: string;
184
+ enabled: boolean;
185
+ env: Record<string, string>;
186
+ }, includeSecrets: boolean): PortableMcpSection;
187
+ /**
188
+ * 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
189
+ * 只改 `/attachments/<chatId>/<文件名>` 这一段,文件名必须整段匹配。
190
+ */
191
+ export declare function rewriteAttachmentRefs(text: string, name: string, newPath: string): string;
192
+ /** 读一份导出文件。版本或 kind 不对时返回错误文案,不抛。 */
193
+ export declare function previewPortable(raw: unknown): PortablePreview | {
194
+ error: string;
195
+ };
196
+ export declare function readConfigDocument(raw: unknown): PortableConfigDocument | {
197
+ error: string;
198
+ };
199
+ export declare function readChatDocument(raw: unknown): PortableChatDocument | {
200
+ error: string;
201
+ };
202
+ export declare function selectedServerSections(sections: PortableConfigSections, selected: readonly string[] | undefined): ServerSectionId[];
@@ -0,0 +1,318 @@
1
+ /**
2
+ * 对话包 / 配置包的文件格式。纯函数:不读库、不写盘。
3
+ *
4
+ * 文件是一个 JSON 对象。`kind` 区分配置、单条对话和多条对话,`schemaVersion` 只认 1。
5
+ * 钥匙(模型 Key、搜索 Key、MCP 环境变量)默认不进包;`includeSecrets`
6
+ * 为 true 时原样写入,导入方要自己确认。
7
+ */
8
+ export const PORTABLE_SCHEMA_VERSION = 1;
9
+ export const PORTABLE_MESSAGE_LIMIT = 1000;
10
+ export const PORTABLE_CONFIG_KIND = 'steerable-config';
11
+ export const PORTABLE_CHAT_KIND = 'steerable-chat';
12
+ export const PORTABLE_CHATS_KIND = 'steerable-chats';
13
+ export const SERVER_SECTION_IDS = [
14
+ 'llm',
15
+ 'webSearch',
16
+ 'telemetry',
17
+ 'insights',
18
+ 'mcp',
19
+ 'agents',
20
+ 'skills',
21
+ ];
22
+ export const CLIENT_SECTION_IDS = ['appearance', 'execPolicy'];
23
+ const SECTION_LABELS = {
24
+ appearance: '界面',
25
+ execPolicy: '命令权限',
26
+ llm: '本地模型',
27
+ webSearch: '网络搜索',
28
+ telemetry: '遥测',
29
+ insights: '帮助改进产品',
30
+ mcp: 'MCP 服务',
31
+ agents: '智能体',
32
+ skills: '技能引用',
33
+ };
34
+ const MESSAGE_ROLES = new Set(['user', 'assistant', 'system', 'tool']);
35
+ export function isServerSectionId(value) {
36
+ return SERVER_SECTION_IDS.includes(value);
37
+ }
38
+ export function isClientSectionId(value) {
39
+ return CLIENT_SECTION_IDS.includes(value);
40
+ }
41
+ function isRecord(value) {
42
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
43
+ }
44
+ function asString(value) {
45
+ return typeof value === 'string' ? value : undefined;
46
+ }
47
+ /** 读项目登记。缺名字或缺家目录时当作没有项目,不把目录内容读进来。 */
48
+ export function readPortableProject(raw) {
49
+ if (!isRecord(raw))
50
+ return null;
51
+ const name = asString(raw.name)?.trim() ?? '';
52
+ const folderPath = asString(raw.folderPath)?.trim() ?? '';
53
+ if (!name || !folderPath)
54
+ return null;
55
+ const sourceFolders = Array.isArray(raw.sourceFolders)
56
+ ? raw.sourceFolders.filter((item) => typeof item === 'string' && item.trim().length > 0)
57
+ : [];
58
+ return {
59
+ name,
60
+ folderPath,
61
+ sourceFolders,
62
+ trusted: raw.trusted === true,
63
+ };
64
+ }
65
+ function thinkingLabel(mode) {
66
+ if (mode === 'hidden')
67
+ return '隐藏';
68
+ if (mode === 'full')
69
+ return '完整显示';
70
+ if (mode === 'peek')
71
+ return '显示5行';
72
+ return '未识别的显示档位';
73
+ }
74
+ function execLabel(policy) {
75
+ if (policy === 'full')
76
+ return '完整权限';
77
+ if (policy === 'workspace')
78
+ return '工作区';
79
+ return '未识别的权限';
80
+ }
81
+ export function llmSectionFromSettings(settings, includeSecrets) {
82
+ const apiKey = asString(settings.apiKey);
83
+ const includeKey = includeSecrets && Boolean(apiKey);
84
+ return {
85
+ provider: asString(settings.provider),
86
+ vendorId: asString(settings.vendorId),
87
+ model: asString(settings.model),
88
+ baseUrl: asString(settings.baseUrl),
89
+ temperature: typeof settings.temperature === 'number' ? settings.temperature : undefined,
90
+ systemPrompt: asString(settings.systemPrompt),
91
+ maxTotalTokens: typeof settings.maxTotalTokens === 'number' ? settings.maxTotalTokens : undefined,
92
+ compat: settings.compat,
93
+ presets: settings.presets,
94
+ execTimeoutSeconds: typeof settings.execTimeoutSeconds === 'number' ? settings.execTimeoutSeconds : undefined,
95
+ apiKeyIncluded: includeKey,
96
+ ...(includeKey ? { apiKey } : {}),
97
+ };
98
+ }
99
+ export function webSearchSectionFromSettings(settings, includeSecrets) {
100
+ const apiKey = typeof settings.apiKey === 'string' ? settings.apiKey : undefined;
101
+ const includeKey = includeSecrets && Boolean(apiKey);
102
+ return {
103
+ provider: settings.provider,
104
+ apiKeyIncluded: includeKey,
105
+ ...(includeKey ? { apiKey } : {}),
106
+ };
107
+ }
108
+ 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
+ };
118
+ }
119
+ /**
120
+ * 把消息正文和 metadata 里的旧附件绝对路径换成新会话里的路径。
121
+ * 只改 `/attachments/<chatId>/<文件名>` 这一段,文件名必须整段匹配。
122
+ */
123
+ export function rewriteAttachmentRefs(text, name, newPath) {
124
+ if (!name || !text.includes('/attachments/'))
125
+ return text;
126
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
127
+ const boundary = '[^\\s`\'"]';
128
+ const end = '[\\s`\'"]';
129
+ return text.replace(new RegExp(`${boundary}*/attachments/${boundary}+/${escaped}(?=${end}|$)`, 'g'), () => newPath);
130
+ }
131
+ function sectionPreview(id, value) {
132
+ const label = SECTION_LABELS[id];
133
+ const clientOnly = isClientSectionId(id);
134
+ if (id === 'llm' && isRecord(value)) {
135
+ const model = asString(value.model) || '未填模型';
136
+ const vendor = asString(value.vendorId) || asString(value.provider) || '模型';
137
+ const hasSecret = value.apiKeyIncluded === true;
138
+ return {
139
+ id,
140
+ label,
141
+ detail: `${vendor} · ${model}${hasSecret ? ' · 含钥匙' : ''}`,
142
+ clientOnly,
143
+ hasSecret,
144
+ };
145
+ }
146
+ if (id === 'webSearch' && isRecord(value)) {
147
+ const hasSecret = value.apiKeyIncluded === true;
148
+ return {
149
+ id,
150
+ label,
151
+ detail: `${asString(value.provider) || '搜索'}${hasSecret ? ' · 含钥匙' : ''}`,
152
+ clientOnly,
153
+ hasSecret,
154
+ };
155
+ }
156
+ if (id === 'mcp' && Array.isArray(value)) {
157
+ const hasSecret = value.some((item) => isRecord(item) && item.envIncluded === true);
158
+ return {
159
+ id,
160
+ label,
161
+ detail: `${value.length} 个服务${hasSecret ? ' · 含环境变量' : ''}`,
162
+ clientOnly,
163
+ hasSecret,
164
+ };
165
+ }
166
+ if (id === 'agents' && Array.isArray(value)) {
167
+ return { id, label, detail: `${value.length} 个自定义智能体`, clientOnly, hasSecret: false };
168
+ }
169
+ if (id === 'skills' && isRecord(value) && Array.isArray(value.names)) {
170
+ return { id, label, detail: `${value.names.length} 个技能名`, clientOnly, hasSecret: false };
171
+ }
172
+ if (id === 'telemetry' && isRecord(value)) {
173
+ return {
174
+ id,
175
+ label,
176
+ detail: asString(value.endpoint) || '未配置端点',
177
+ clientOnly,
178
+ hasSecret: false,
179
+ };
180
+ }
181
+ if (id === 'insights' && isRecord(value)) {
182
+ return { id, label, detail: '同意项与资料', clientOnly, hasSecret: false };
183
+ }
184
+ if (id === 'appearance' && isRecord(value)) {
185
+ return { id, label, detail: thinkingLabel(value.thinkingDisplay), clientOnly, hasSecret: false };
186
+ }
187
+ if (id === 'execPolicy' && isRecord(value)) {
188
+ return { id, label, detail: execLabel(value.policy), clientOnly, hasSecret: false };
189
+ }
190
+ return null;
191
+ }
192
+ function previewConfig(doc) {
193
+ const sections = isRecord(doc.sections) ? doc.sections : null;
194
+ if (!sections)
195
+ return { error: '配置包缺少 sections' };
196
+ const listed = [];
197
+ for (const id of [...CLIENT_SECTION_IDS, ...SERVER_SECTION_IDS]) {
198
+ if (!(id in sections))
199
+ continue;
200
+ const row = sectionPreview(id, sections[id]);
201
+ if (row)
202
+ listed.push(row);
203
+ }
204
+ return {
205
+ kind: PORTABLE_CONFIG_KIND,
206
+ includeSecrets: doc.includeSecrets === true,
207
+ sections: listed,
208
+ };
209
+ }
210
+ function previewChat(doc) {
211
+ const chat = isRecord(doc.chat) ? doc.chat : null;
212
+ if (!chat || typeof chat.title !== 'string' || !chat.title.trim()) {
213
+ return { error: '对话包缺少标题' };
214
+ }
215
+ if (!Array.isArray(doc.messages))
216
+ return { error: '对话包缺少 messages' };
217
+ const attachments = Array.isArray(doc.attachments) ? doc.attachments : [];
218
+ const withData = attachments.filter((item) => isRecord(item) && typeof item.data === 'string' && !item.omitted);
219
+ const omitted = attachments.filter((item) => isRecord(item) && item.omitted === true);
220
+ const project = readPortableProject(doc.project);
221
+ return {
222
+ kind: PORTABLE_CHAT_KIND,
223
+ includeSecrets: false,
224
+ sections: [],
225
+ chat: {
226
+ title: chat.title.trim(),
227
+ messageCount: doc.messages.length,
228
+ attachmentCount: withData.length,
229
+ omittedAttachmentCount: omitted.length,
230
+ truncated: doc.truncated === true,
231
+ ...(project ? { projectName: project.name, projectCount: 1 } : {}),
232
+ },
233
+ };
234
+ }
235
+ /** 读一份导出文件。版本或 kind 不对时返回错误文案,不抛。 */
236
+ export function previewPortable(raw) {
237
+ if (!isRecord(raw))
238
+ return { error: '文件不是 JSON 对象' };
239
+ if (raw.schemaVersion !== PORTABLE_SCHEMA_VERSION) {
240
+ return { error: '无法读取这份文件,需要更新客户端' };
241
+ }
242
+ if (raw.kind === PORTABLE_CONFIG_KIND)
243
+ return previewConfig(raw);
244
+ if (raw.kind === PORTABLE_CHAT_KIND)
245
+ return previewChat(raw);
246
+ if (raw.kind === PORTABLE_CHATS_KIND)
247
+ return previewChats(raw);
248
+ return { error: '无法识别的导出文件' };
249
+ }
250
+ function previewChats(doc) {
251
+ if (!Array.isArray(doc.chats) || doc.chats.length === 0)
252
+ return { error: '对话包里没有对话' };
253
+ const titles = [];
254
+ const projectNames = new Set();
255
+ let messageCount = 0;
256
+ let attachmentCount = 0;
257
+ let omittedAttachmentCount = 0;
258
+ let truncated = false;
259
+ for (const item of doc.chats) {
260
+ if (!isRecord(item))
261
+ return { error: '对话包里有无法读取的会话' };
262
+ const one = previewChat(item);
263
+ if ('error' in one || !one.chat) {
264
+ return 'error' in one ? one : { error: '对话包里有无法读取的会话' };
265
+ }
266
+ titles.push(one.chat.title);
267
+ if (one.chat.projectName)
268
+ projectNames.add(one.chat.projectName);
269
+ messageCount += one.chat.messageCount;
270
+ attachmentCount += one.chat.attachmentCount;
271
+ omittedAttachmentCount += one.chat.omittedAttachmentCount;
272
+ truncated = truncated || one.chat.truncated;
273
+ }
274
+ const count = titles.length;
275
+ const projectCount = projectNames.size;
276
+ const onlyProject = projectCount === 1 ? [...projectNames][0] : undefined;
277
+ return {
278
+ kind: PORTABLE_CHATS_KIND,
279
+ includeSecrets: false,
280
+ sections: [],
281
+ chat: {
282
+ title: count === 1 ? (titles[0] ?? '未命名') : `${count} 条对话`,
283
+ messageCount,
284
+ attachmentCount,
285
+ omittedAttachmentCount,
286
+ truncated,
287
+ count,
288
+ ...(projectCount > 0 ? { projectCount, ...(onlyProject ? { projectName: onlyProject } : {}) } : {}),
289
+ },
290
+ };
291
+ }
292
+ export function readConfigDocument(raw) {
293
+ const preview = previewPortable(raw);
294
+ if ('error' in preview)
295
+ return preview;
296
+ if (preview.kind !== PORTABLE_CONFIG_KIND)
297
+ return { error: '这是对话包,不能当作配置导入' };
298
+ return raw;
299
+ }
300
+ export function readChatDocument(raw) {
301
+ const preview = previewPortable(raw);
302
+ if ('error' in preview)
303
+ return preview;
304
+ if (preview.kind !== PORTABLE_CHAT_KIND)
305
+ return { error: '这是配置包,请在设置页导入' };
306
+ const doc = raw;
307
+ const messages = doc.messages.filter((message) => message &&
308
+ MESSAGE_ROLES.has(message.role) &&
309
+ typeof message.content === 'string');
310
+ return { ...doc, messages };
311
+ }
312
+ export function selectedServerSections(sections, selected) {
313
+ const present = SERVER_SECTION_IDS.filter((id) => sections[id] !== undefined);
314
+ if (!selected)
315
+ return [...present];
316
+ const allow = new Set(selected);
317
+ return present.filter((id) => allow.has(id));
318
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * 对话包 / 配置包的读写。路由只负责在产品开关打开时把请求转进来。
3
+ */
4
+ import { type PortableChatDocument, type PortableConfigDocument } from './portable-bundle.js';
5
+ import type { ProjectRegistry } from '../project-registry.js';
6
+ import type { McpServerRegistry } from '../mcp-server-registry.js';
7
+ import type { ScopedStore } from '../storage/scoped-store.js';
8
+ export interface PortableResponse {
9
+ status: number;
10
+ data: unknown;
11
+ }
12
+ export declare function exportConfigDocument(store: ScopedStore, mcp: McpServerRegistry | null, includeSecrets: boolean): Promise<PortableConfigDocument>;
13
+ export declare function exportChatDocument(store: ScopedStore, chatId: string, projects?: ProjectRegistry | null): Promise<PortableChatDocument | null>;
14
+ export declare function importConfigDocument(store: ScopedStore, mcp: McpServerRegistry | null, raw: unknown, selected: readonly string[] | undefined): Promise<PortableResponse>;
15
+ export declare function handlePortableRequest(input: {
16
+ method: string;
17
+ pathname: string;
18
+ includeSecrets: boolean;
19
+ body: unknown;
20
+ store: ScopedStore;
21
+ mcp: McpServerRegistry | null;
22
+ projects: ProjectRegistry | null;
23
+ }): Promise<PortableResponse>;