@steerable/agent-shell 0.6.47 → 0.6.49

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 (36) hide show
  1. package/dist/app-update.d.ts +10 -0
  2. package/dist/app-update.js +14 -0
  3. package/dist/host/runtime.js +3 -3
  4. package/dist/host-tools.d.ts +1 -1
  5. package/dist/host-tools.js +1 -0
  6. package/dist/llm/sidecar-provider.d.ts +0 -4
  7. package/dist/llm/sidecar-provider.js +0 -4
  8. package/dist/local-backend/coreloop-stream.d.ts +1 -3
  9. package/dist/local-backend/coreloop-stream.js +1 -3
  10. package/dist/local-backend/portable-bundle.d.ts +202 -0
  11. package/dist/local-backend/portable-bundle.js +318 -0
  12. package/dist/local-backend/portable-service.d.ts +23 -0
  13. package/dist/local-backend/portable-service.js +469 -0
  14. package/dist/local-backend/router.js +20 -0
  15. package/dist/local-backend/skills/85-local-exec/SKILL.md +2 -0
  16. package/dist/local-backend/skills/86-proactive-coding/SKILL.md +1 -1
  17. package/dist/local-backend/turn-files.d.ts +20 -0
  18. package/dist/local-backend/turn-files.js +162 -5
  19. package/dist/main.js +21 -1
  20. package/dist/preload.cjs +10 -0
  21. package/dist/preload.js +10 -0
  22. package/dist/present-files.d.ts +64 -0
  23. package/dist/present-files.js +136 -0
  24. package/dist/product-config.d.ts +6 -0
  25. package/dist/product-config.js +4 -0
  26. package/dist/server/start.js +7 -1
  27. package/dist/sidecar/boot.d.ts +19 -4
  28. package/dist/sidecar/boot.js +54 -20
  29. package/dist/sidecar/handle.d.ts +5 -8
  30. package/dist/sidecar/handle.js +6 -10
  31. package/dist/sidecar/supervisor.d.ts +1 -9
  32. package/dist/sidecar/supervisor.js +31 -81
  33. package/dist/sidecar/types.d.ts +0 -2
  34. package/dist/tool-router.d.ts +19 -2
  35. package/dist/tool-router.js +63 -2
  36. package/package.json +4 -4
@@ -0,0 +1,469 @@
1
+ /**
2
+ * 对话包 / 配置包的读写。路由只负责在产品开关打开时把请求转进来。
3
+ */
4
+ import { readdir, readFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { normalizeToolPolicy } from './agent-capability.js';
7
+ import { PORTABLE_CHAT_KIND, PORTABLE_CHATS_KIND, PORTABLE_CONFIG_KIND, PORTABLE_MESSAGE_LIMIT, PORTABLE_SCHEMA_VERSION, isClientSectionId, llmSectionFromSettings, mcpSectionFromServer, previewPortable, readChatDocument, readConfigDocument, readPortableProject, rewriteAttachmentRefs, selectedServerSections, webSearchSectionFromSettings, } from './portable-bundle.js';
8
+ import { isProductLlmLocked } from '../host-tools-runtime.js';
9
+ import { ensureProjectHome } from '../project-home.js';
10
+ import { getUserDataDir } from '../runtime.js';
11
+ import { chatAttachmentsDirPath, isValidChatAttachmentsKey, saveAttachmentFiles, } from '../attachments.js';
12
+ import { sanitizeCompatOverrides, sanitizeLlmProvider, sanitizePresetsChoice, sanitizeVendorId, } from '../storage/llm-settings.js';
13
+ const ATTACHMENT_FILE_BYTES = 8 * 1024 * 1024;
14
+ const ATTACHMENT_TOTAL_BYTES = 32 * 1024 * 1024;
15
+ function fail(status, detail) {
16
+ return { status, data: { detail, error: detail } };
17
+ }
18
+ function exportedAt() {
19
+ return new Date().toISOString();
20
+ }
21
+ async function listUserSkillNames() {
22
+ const dir = path.join(getUserDataDir(), 'skills');
23
+ try {
24
+ const entries = await readdir(dir, { withFileTypes: true });
25
+ return entries
26
+ .filter((entry) => entry.isDirectory())
27
+ .map((entry) => entry.name)
28
+ .sort((a, b) => a.localeCompare(b));
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ export async function exportConfigDocument(store, mcp, includeSecrets) {
35
+ const sections = {};
36
+ const llm = await store.getLlmSettings();
37
+ if (llm) {
38
+ sections.llm = llmSectionFromSettings(llm, includeSecrets);
39
+ }
40
+ const webSearch = await store.getWebSearchSettings();
41
+ if (webSearch)
42
+ sections.webSearch = webSearchSectionFromSettings(webSearch, includeSecrets);
43
+ const telemetry = await store.getTelemetrySettings();
44
+ if (telemetry) {
45
+ sections.telemetry = {
46
+ endpoint: telemetry.endpoint,
47
+ privacyMode: telemetry.privacyMode,
48
+ serviceName: telemetry.serviceName,
49
+ };
50
+ }
51
+ const insights = await store.getInsightsSettings();
52
+ if (insights) {
53
+ sections.insights = {
54
+ shareBehavior: insights.shareBehavior,
55
+ shareConversation: insights.shareConversation,
56
+ shareProfile: insights.shareProfile,
57
+ promptedAt: typeof insights.promptedAt === 'string' ? insights.promptedAt : undefined,
58
+ apiBase: insights.apiBase,
59
+ profile: { ...insights.profile },
60
+ };
61
+ }
62
+ if (mcp) {
63
+ const servers = mcp.list();
64
+ if (servers.length > 0) {
65
+ sections.mcp = servers.map((server) => mcpSectionFromServer(server, includeSecrets));
66
+ }
67
+ }
68
+ const agents = (await store.listChatAgents(true)).filter((agent) => !agent.isBuiltin);
69
+ if (agents.length > 0) {
70
+ sections.agents = agents.map((agent) => ({
71
+ name: agent.name,
72
+ slug: agent.slug,
73
+ icon: agent.icon,
74
+ color: agent.color,
75
+ description: agent.description,
76
+ rolePrompt: agent.rolePrompt,
77
+ forbiddenPrompt: agent.forbiddenPrompt,
78
+ skillIds: [...agent.skillIds],
79
+ toolPolicy: agent.toolPolicy,
80
+ allowExternalSkills: agent.allowExternalSkills,
81
+ loadAllSkills: agent.loadAllSkills,
82
+ isArchived: agent.isArchived,
83
+ sortOrder: agent.sortOrder,
84
+ }));
85
+ }
86
+ const skillNames = await listUserSkillNames();
87
+ if (skillNames.length > 0)
88
+ sections.skills = { names: skillNames };
89
+ return {
90
+ kind: PORTABLE_CONFIG_KIND,
91
+ schemaVersion: PORTABLE_SCHEMA_VERSION,
92
+ exportedAt: exportedAt(),
93
+ includeSecrets,
94
+ sections,
95
+ };
96
+ }
97
+ async function readChatAttachments(chatId) {
98
+ if (!isValidChatAttachmentsKey(chatId))
99
+ return [];
100
+ const dir = chatAttachmentsDirPath(chatId);
101
+ let names = [];
102
+ try {
103
+ const entries = await readdir(dir, { withFileTypes: true });
104
+ names = entries.filter((entry) => entry.isFile()).map((entry) => entry.name);
105
+ }
106
+ catch {
107
+ return [];
108
+ }
109
+ const attachments = [];
110
+ let total = 0;
111
+ for (const name of names) {
112
+ const full = path.join(dir, name);
113
+ try {
114
+ const bytes = await readFile(full);
115
+ if (bytes.byteLength > ATTACHMENT_FILE_BYTES || total + bytes.byteLength > ATTACHMENT_TOTAL_BYTES) {
116
+ attachments.push({ name, size: bytes.byteLength, omitted: true });
117
+ continue;
118
+ }
119
+ total += bytes.byteLength;
120
+ attachments.push({ name, size: bytes.byteLength, data: bytes.toString('base64') });
121
+ }
122
+ catch {
123
+ attachments.push({ name, size: 0, omitted: true });
124
+ }
125
+ }
126
+ return attachments;
127
+ }
128
+ export async function exportChatDocument(store, chatId, projects = null) {
129
+ const chat = await store.getChat(chatId);
130
+ if (!chat)
131
+ return null;
132
+ const newestFirst = await store.listMessages(chatId, PORTABLE_MESSAGE_LIMIT);
133
+ const messages = newestFirst.slice().reverse().map((message) => ({
134
+ role: message.role,
135
+ content: message.content,
136
+ messageMetadata: message.messageMetadata,
137
+ createdAt: message.createdAt,
138
+ }));
139
+ return {
140
+ kind: PORTABLE_CHAT_KIND,
141
+ schemaVersion: PORTABLE_SCHEMA_VERSION,
142
+ exportedAt: exportedAt(),
143
+ chat: {
144
+ title: chat.title,
145
+ agentId: chat.agentId,
146
+ isPinned: chat.isPinned,
147
+ systemPrompt: chat.systemPrompt,
148
+ pinnedRefs: chat.pinnedRefs,
149
+ },
150
+ ...(linkedProject(projects, chat.projectId) ?? {}),
151
+ messages,
152
+ truncated: newestFirst.length >= PORTABLE_MESSAGE_LIMIT,
153
+ attachments: await readChatAttachments(chatId),
154
+ };
155
+ }
156
+ async function applyLlm(store, section) {
157
+ if (isProductLlmLocked())
158
+ return '本产品的模型由部署配置固定';
159
+ const current = await store.getLlmSettings();
160
+ const next = {
161
+ provider: sanitizeLlmProvider(section.provider),
162
+ vendorId: sanitizeVendorId(section.vendorId),
163
+ model: section.model?.trim() || current?.model || 'deepseek-chat',
164
+ baseUrl: section.baseUrl,
165
+ apiKey: section.apiKeyIncluded ? section.apiKey : current?.apiKey,
166
+ temperature: section.temperature,
167
+ systemPrompt: section.systemPrompt,
168
+ maxTotalTokens: section.maxTotalTokens,
169
+ compat: sanitizeCompatOverrides(section.compat),
170
+ presets: sanitizePresetsChoice(section.presets),
171
+ execTimeoutSeconds: typeof section.execTimeoutSeconds === 'number' && section.execTimeoutSeconds > 0
172
+ ? Math.floor(section.execTimeoutSeconds)
173
+ : undefined,
174
+ };
175
+ await store.setLlmSettings(next);
176
+ return null;
177
+ }
178
+ async function applyMcp(mcp, servers) {
179
+ if (!mcp)
180
+ return 'MCP 注册表不可用';
181
+ for (const server of servers) {
182
+ const name = server.name?.trim();
183
+ const command = server.command?.trim();
184
+ if (!name || !command)
185
+ 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
+ };
194
+ const existing = mcp.list().find((item) => item.name.toLowerCase() === name.toLowerCase());
195
+ if (existing)
196
+ mcp.update(existing.id, input);
197
+ else
198
+ mcp.create({ ...input, env: input.env ?? {} });
199
+ }
200
+ return null;
201
+ }
202
+ async function applyAgents(store, incoming) {
203
+ const notes = [];
204
+ const current = await store.listChatAgents(true);
205
+ for (const agent of incoming) {
206
+ const name = agent.name?.trim();
207
+ if (!name)
208
+ continue;
209
+ if (current.some((item) => item.isBuiltin && item.name === name)) {
210
+ notes.push(`跳过与内置智能体同名的「${name}」`);
211
+ continue;
212
+ }
213
+ const fields = {
214
+ slug: agent.slug ?? null,
215
+ icon: agent.icon ?? null,
216
+ color: agent.color ?? null,
217
+ description: agent.description ?? null,
218
+ rolePrompt: agent.rolePrompt ?? null,
219
+ forbiddenPrompt: agent.forbiddenPrompt ?? null,
220
+ skillIds: Array.isArray(agent.skillIds) ? agent.skillIds.map((id) => String(id)) : [],
221
+ toolPolicy: normalizeToolPolicy(agent.toolPolicy),
222
+ allowExternalSkills: agent.allowExternalSkills !== false,
223
+ loadAllSkills: agent.loadAllSkills === true,
224
+ isArchived: agent.isArchived === true,
225
+ sortOrder: typeof agent.sortOrder === 'number' ? agent.sortOrder : 0,
226
+ isBuiltin: false,
227
+ };
228
+ const found = current.find((item) => !item.isBuiltin && item.name === name);
229
+ if (found) {
230
+ await store.updateChatAgent(found.id, fields);
231
+ }
232
+ else {
233
+ const created = await store.createChatAgent({ name, ...fields });
234
+ current.push(created);
235
+ }
236
+ }
237
+ return notes;
238
+ }
239
+ async function applySkills(names) {
240
+ const present = new Set(await listUserSkillNames());
241
+ return names.filter((name) => typeof name === 'string' && name && !present.has(name));
242
+ }
243
+ export async function importConfigDocument(store, mcp, raw, selected) {
244
+ const doc = readConfigDocument(raw);
245
+ if ('error' in doc)
246
+ return fail(400, doc.error);
247
+ const ids = selectedServerSections(doc.sections, selected);
248
+ const applied = [];
249
+ const skipped = [];
250
+ const notes = [];
251
+ let missingSkills = [];
252
+ for (const id of ids) {
253
+ if (id === 'llm' && doc.sections.llm) {
254
+ const reason = await applyLlm(store, doc.sections.llm);
255
+ if (reason)
256
+ skipped.push({ id, reason });
257
+ else
258
+ applied.push(id);
259
+ }
260
+ else if (id === 'webSearch' && doc.sections.webSearch) {
261
+ const current = await store.getWebSearchSettings();
262
+ const section = doc.sections.webSearch;
263
+ await store.setWebSearchSettings({
264
+ provider: section.provider === 'ddg' ? 'ddg' : 'tavily',
265
+ apiKey: section.apiKeyIncluded ? section.apiKey : current?.apiKey,
266
+ });
267
+ applied.push(id);
268
+ }
269
+ else if (id === 'telemetry' && doc.sections.telemetry) {
270
+ const telemetry = doc.sections.telemetry;
271
+ await store.setTelemetrySettings({
272
+ endpoint: telemetry.endpoint,
273
+ privacyMode: telemetry.privacyMode === 'full' ? 'full' : 'metadata',
274
+ serviceName: telemetry.serviceName,
275
+ });
276
+ applied.push(id);
277
+ }
278
+ else if (id === 'insights' && doc.sections.insights) {
279
+ await store.setInsightsSettings(doc.sections.insights);
280
+ applied.push(id);
281
+ }
282
+ else if (id === 'mcp' && doc.sections.mcp) {
283
+ const reason = await applyMcp(mcp, doc.sections.mcp);
284
+ if (reason)
285
+ skipped.push({ id, reason });
286
+ else
287
+ applied.push(id);
288
+ }
289
+ else if (id === 'agents' && doc.sections.agents) {
290
+ notes.push(...(await applyAgents(store, doc.sections.agents)));
291
+ applied.push(id);
292
+ }
293
+ else if (id === 'skills' && doc.sections.skills) {
294
+ missingSkills = await applySkills(doc.sections.skills.names ?? []);
295
+ applied.push(id);
296
+ }
297
+ }
298
+ const clientSections = (selected ?? []).filter(isClientSectionId);
299
+ return {
300
+ status: 200,
301
+ data: { applied, skipped, notes, missingSkills, clientSections },
302
+ };
303
+ }
304
+ function linkedProject(projects, projectId) {
305
+ if (!projects || !projectId)
306
+ return null;
307
+ const project = projects.get(projectId);
308
+ if (!project)
309
+ return null;
310
+ return {
311
+ project: {
312
+ name: project.name,
313
+ folderPath: project.folderPath,
314
+ sourceFolders: project.sourceFolders ?? [],
315
+ trusted: project.trusted === true,
316
+ },
317
+ };
318
+ }
319
+ /**
320
+ * 同名项目沿用本机已有记录,不改它的目录。没有同名项目时按导出的路径登记,
321
+ * 路径不存在就建一个空目录。源文件夹和其他目录里的文件都不复制。
322
+ */
323
+ function bindImportedProject(projects, raw) {
324
+ if (!projects)
325
+ return null;
326
+ const project = readPortableProject(raw);
327
+ if (!project)
328
+ return null;
329
+ const existing = projects.get(project.name);
330
+ if (existing)
331
+ return existing.id;
332
+ ensureProjectHome(project.folderPath);
333
+ const created = projects.create({
334
+ name: project.name,
335
+ folderPath: project.folderPath,
336
+ sourceFolders: project.sourceFolders,
337
+ });
338
+ if (project.trusted)
339
+ projects.setTrusted(created.id, true);
340
+ return created.id;
341
+ }
342
+ async function importChatDocument(store, raw, projects) {
343
+ const doc = readChatDocument(raw);
344
+ if ('error' in doc)
345
+ return { response: fail(400, doc.error) };
346
+ const created = await store.createChat(doc.chat.title.trim(), doc.chat.agentId, bindImportedProject(projects, doc.project));
347
+ const pinnedRefs = Array.isArray(doc.chat.pinnedRefs) ? doc.chat.pinnedRefs : null;
348
+ await store.updateChat(created.id, {
349
+ isPinned: doc.chat.isPinned === true,
350
+ systemPrompt: typeof doc.chat.systemPrompt === 'string' ? doc.chat.systemPrompt : null,
351
+ pinnedRefs,
352
+ });
353
+ const pathByName = new Map();
354
+ const files = (doc.attachments ?? []).filter((file) => file && typeof file.data === 'string' && !file.omitted);
355
+ if (files.length > 0) {
356
+ const saved = await saveAttachmentFiles(created.id, files.map((file) => ({ name: file.name, data: file.data })));
357
+ for (const file of saved.files) {
358
+ if (file.path)
359
+ pathByName.set(file.name, file.path);
360
+ }
361
+ }
362
+ const messages = doc.messages.slice(0, PORTABLE_MESSAGE_LIMIT);
363
+ for (const message of messages) {
364
+ let content = message.content;
365
+ let metadata = message.messageMetadata;
366
+ for (const [name, nextPath] of pathByName) {
367
+ content = rewriteAttachmentRefs(content, name, nextPath);
368
+ if (metadata)
369
+ metadata = rewriteAttachmentRefs(metadata, name, nextPath);
370
+ }
371
+ await store.addMessage(created.id, message.role, content, metadata);
372
+ }
373
+ const written = {
374
+ chatId: created.id,
375
+ title: created.title,
376
+ messageCount: messages.length,
377
+ };
378
+ return {
379
+ written,
380
+ response: {
381
+ status: 200,
382
+ data: { ...written, attachmentsSaved: pathByName.size },
383
+ },
384
+ };
385
+ }
386
+ async function importSubmittedChats(store, raw, projects) {
387
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw) || raw.kind !== PORTABLE_CHATS_KIND) {
388
+ return (await importChatDocument(store, raw, projects)).response;
389
+ }
390
+ const preview = previewPortable(raw);
391
+ if ('error' in preview)
392
+ return fail(400, preview.error);
393
+ const bundle = raw;
394
+ if (!Array.isArray(bundle.chats))
395
+ return fail(400, '对话包里没有对话');
396
+ const docs = [];
397
+ for (const item of bundle.chats) {
398
+ const doc = readChatDocument(item);
399
+ if ('error' in doc)
400
+ return fail(400, doc.error);
401
+ docs.push(doc);
402
+ }
403
+ const imported = [];
404
+ for (const doc of docs) {
405
+ const result = await importChatDocument(store, doc, projects);
406
+ if (!result.written)
407
+ return result.response;
408
+ imported.push(result.written);
409
+ }
410
+ const messageCount = imported.reduce((sum, item) => sum + item.messageCount, 0);
411
+ return {
412
+ status: 200,
413
+ data: {
414
+ chatId: imported[0]?.chatId ?? '',
415
+ title: imported.length === 1 ? (imported[0]?.title ?? '') : `${imported.length} 条对话`,
416
+ messageCount,
417
+ chatCount: imported.length,
418
+ },
419
+ };
420
+ }
421
+ function readBodyDocument(body) {
422
+ if (!body || typeof body !== 'object' || Array.isArray(body))
423
+ return null;
424
+ const record = body;
425
+ if (!('document' in record))
426
+ return null;
427
+ const sections = Array.isArray(record.sections)
428
+ ? record.sections.filter((item) => typeof item === 'string')
429
+ : undefined;
430
+ return { document: record.document, sections };
431
+ }
432
+ export async function handlePortableRequest(input) {
433
+ const { method, pathname, store, mcp } = input;
434
+ if (method === 'GET' && pathname === '/api/v2/portable/config') {
435
+ return {
436
+ status: 200,
437
+ data: await exportConfigDocument(store, mcp, input.includeSecrets),
438
+ };
439
+ }
440
+ if (method === 'POST' && pathname === '/api/v2/portable/preview') {
441
+ const wrapped = readBodyDocument(input.body);
442
+ if (!wrapped)
443
+ return fail(400, '缺少 document');
444
+ const preview = previewPortable(wrapped.document);
445
+ if ('error' in preview)
446
+ return fail(400, preview.error);
447
+ return { status: 200, data: preview };
448
+ }
449
+ if (method === 'POST' && pathname === '/api/v2/portable/config') {
450
+ const wrapped = readBodyDocument(input.body);
451
+ if (!wrapped)
452
+ return fail(400, '缺少 document');
453
+ return importConfigDocument(store, mcp, wrapped.document, wrapped.sections);
454
+ }
455
+ if (method === 'POST' && pathname === '/api/v2/portable/chats') {
456
+ const wrapped = readBodyDocument(input.body);
457
+ if (!wrapped)
458
+ return fail(400, '缺少 document');
459
+ return importSubmittedChats(store, wrapped.document, input.projects);
460
+ }
461
+ const chatMatch = pathname.match(/^\/api\/v2\/chats\/([^/]+)\/portable$/);
462
+ if (chatMatch && method === 'GET') {
463
+ const doc = await exportChatDocument(store, decodeURIComponent(chatMatch[1]), input.projects);
464
+ if (!doc)
465
+ return fail(404, 'Chat not found');
466
+ return { status: 200, data: doc };
467
+ }
468
+ return fail(404, 'Not found');
469
+ }
@@ -37,6 +37,8 @@ import { formatHistoryForSummary, truncateMiddle, } from './context-compactor.js
37
37
  import { planRegenerateTruncate, resolveRegenerateContext, resolveRegenerateForkOrdinal, } from './regenerate-helper.js';
38
38
  import { resolveBranchActivation } from './branch-helper.js';
39
39
  import { detectInterruptedTurn } from './interrupted-helper.js';
40
+ import { handlePortableRequest } from './portable-service.js';
41
+ import { isPortableProduct } from '../product-config.js';
40
42
  import { dropCurrentUserMessage } from './history-helper.js';
41
43
  import { parseImageAttachments, processImageAttachments } from '../image-attachment.js';
42
44
  import { chatAttachmentsDirPath } from '../attachments.js';
@@ -226,6 +228,24 @@ export class LocalBackendRouter {
226
228
  }
227
229
  return { status: 200, data: buildLocalApiUser() };
228
230
  }
231
+ if (pathname === '/api/v2/portable/config' ||
232
+ pathname === '/api/v2/portable/preview' ||
233
+ pathname === '/api/v2/portable/chats' ||
234
+ /^\/api\/v2\/chats\/[^/]+\/portable$/.test(pathname)) {
235
+ if (!isPortableProduct())
236
+ return this.notFound('Not found');
237
+ return handlePortableRequest({
238
+ method,
239
+ pathname,
240
+ includeSecrets: url.searchParams.get('includeSecrets') === '1',
241
+ body: request.body,
242
+ store: this.store,
243
+ mcp: this.toolRouter.mcpRegistry ?? null,
244
+ projects: getResolvedHostTools().projects.capability
245
+ ? (this.toolRouter.projectRegistry ?? null)
246
+ : null,
247
+ });
248
+ }
229
249
  // Desktop Agent —— 在本地模式下虚拟一个永远在线的 agent,所有 exec 直接走 ToolRouter
230
250
  if (method === 'GET' && pathname === '/api/v2/agents') {
231
251
  const agent = buildLocalAgent();
@@ -19,6 +19,7 @@ match: any
19
19
  | 写入 / 修改文件 | `local_write_file`(`path` + `content`) |
20
20
  | 打开文件 / URL | `local_open_path` |
21
21
  | 调用已注册脚本 | `local_list_scripts` → `local_run_script` |
22
+ | 声明本轮交付给用户的最终文件 | `present_files`(`files: [{ path, description? }]`,最多 4 个) |
22
23
 
23
24
  ## 规则
24
25
 
@@ -26,6 +27,7 @@ match: any
26
27
  - 命令可能改文件、装包、删数据时(destructive 类操作),**先**简单告诉用户你要执行什么,**再**发 tool_call;不要中间停手等用户确认(用户已经请求了)。
27
28
  - `local_exec_shell` 返回包含 `stdout` / `stderr` / `exitCode`。非 0 退出码视为失败:把 stderr 摘要给用户,并建议下一步。
28
29
  - 路径不存在 / 权限不足时,把 fs 错误原文告诉用户,**不要**反复重试同一个路径。
30
+ - **交付文件要声明**:用户要的产出(表格、PPT、文档、报告、图片、导出包)写好后,在最终回复前调用 `present_files` 声明它们,脚本/命令生成的也一样。辅助脚本、临时文件、只为自查渲染效果导出的预览图、中间导出副本**不要**声明——它们会自动列在「已编辑文件」里。
29
31
 
30
32
 
31
33
  - **超时 ≠ 失败,严禁原样重跑启动命令**:返回里出现 `timedOut: true` 时,只是 shell 等待被掐断——如果命令是启动 GUI / 长时间运行的程序(如 Qt 界面软件),该程序**很可能仍在运行**,用户可能正在里面操作。此时**不要**再次执行同一条启动命令(会把程序重复启动多次)。正确做法:先确认进程是否在运行(如 `Get-Process`);确实需要长时间等待时传更大的 `timeout` 参数;启动 GUI 程序时优先用不阻塞的方式(PowerShell `Start-Process "app.exe"`),启动成功即视为完成,不要等它退出。
@@ -47,5 +47,5 @@ match: any
47
47
 
48
48
  - **没跑过的代码不要说"运行结果如下"。** 只有看到 `local_run_snippet` 的真实返回(`exitCode: 0` + `stdout`)才能汇报结果;非 0 退出码就读 `stderr` 改代码重跑(最多迭代 2-3 次,还不行就如实说明卡点)。
49
49
  - 脚本里读写用户文件时,路径用用户提供的真实路径或项目内相对路径;不要编造路径。
50
- - 脚本产生的**交付物**(报表、转换后的文件)要落到用户能找到的地方(项目目录或用户指定路径),并在回复里给出完整路径——scratch 目录是临时区,不是交付位置。
50
+ - 脚本产生的**交付物**(报表、转换后的文件)要落到用户能找到的地方(项目目录或用户指定路径),并在回复里给出完整路径,最终回复前用 `present_files` 声明——scratch 目录是临时区,不是交付位置。
51
51
  - 代码保持简单直接:能 20 行解决就不要写 100 行;不要引入与任务无关的"工程化"。
@@ -36,13 +36,33 @@ export interface TurnFile {
36
36
  kind: 'created' | 'modified';
37
37
  /** 字节数(展示用)。 */
38
38
  size: number;
39
+ /** 新增行数(展示用,如 +110)。 */
40
+ additions?: number;
41
+ /** 删除行数(展示用,如 -0)。 */
42
+ deletions?: number;
43
+ /** 类别:deliverable = present_files 声明的最终交付文件;intermediate = 其余本轮写过的文件。 */
44
+ category?: 'deliverable' | 'intermediate';
45
+ /** present_files 给出的一行说明(交付卡片副标题)。 */
46
+ description?: string;
39
47
  }
40
48
  /** 扫描入参里只需要工具行动的这几个字段(与 router 的 executedActions 行结构对齐)。 */
41
49
  export interface TurnFileAction {
42
50
  tool?: unknown;
43
51
  arguments?: unknown;
52
+ result?: unknown;
44
53
  success?: unknown;
45
54
  }
55
+ /** 交付物扩展名(最终产物,如电子表格、幻灯片、文档、图片、音视频、独立页面、压缩包)。 */
56
+ export declare const DELIVERABLE_EXTENSIONS: Set<string>;
57
+ /** 是否为临时文件 / 办公软件锁定文件(如 ~$ 开头的文件名),产物列表中一律排除。 */
58
+ export declare function isIgnoredFileName(name: string): boolean;
59
+ /** 判定文件是最终交付文件还是中间修改文件。 */
60
+ export declare function classifyFileCategory(filePath: string): 'deliverable' | 'intermediate';
61
+ /** 从统一 diff 文本中统计增删行数。 */
62
+ export declare function parseDiffStats(diffText: string): {
63
+ additions: number;
64
+ deletions: number;
65
+ };
46
66
  /**
47
67
  * 收集本轮产物文件。`sinceMs` 是回合开始的 epoch ms;`projectRoot` 用于把
48
68
  * 写工具的相对路径参数解析成绝对路径(与 local-executor 的项目根解析一致)。