codexmate 0.0.56 → 0.0.57

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 (31) hide show
  1. package/cli.js +680 -129
  2. package/lib/task-orchestrator.js +90 -21
  3. package/lib/task-workspace-chat.js +292 -0
  4. package/package.json +2 -2
  5. package/web-ui/app.js +55 -129
  6. package/web-ui/modules/app.computed.main-tabs.mjs +304 -7
  7. package/web-ui/modules/app.methods.agents.mjs +3 -0
  8. package/web-ui/modules/app.methods.claude-config.mjs +65 -25
  9. package/web-ui/modules/app.methods.navigation.mjs +67 -83
  10. package/web-ui/modules/app.methods.openclaw-editing.mjs +3 -6
  11. package/web-ui/modules/app.methods.session-actions.mjs +1 -12
  12. package/web-ui/modules/app.methods.session-browser.mjs +23 -54
  13. package/web-ui/modules/app.methods.session-trash.mjs +0 -1
  14. package/web-ui/modules/app.methods.startup-claude.mjs +16 -31
  15. package/web-ui/modules/app.methods.task-orchestration.mjs +347 -8
  16. package/web-ui/modules/app.methods.tool-config-permissions.mjs +0 -3
  17. package/web-ui/modules/app.methods.web-ui-preferences.mjs +415 -68
  18. package/web-ui/modules/config-template-confirm-pref.mjs +2 -3
  19. package/web-ui/modules/i18n/locales/en.mjs +146 -26
  20. package/web-ui/modules/i18n/locales/ja.mjs +143 -23
  21. package/web-ui/modules/i18n/locales/vi.mjs +145 -25
  22. package/web-ui/modules/i18n/locales/zh-tw.mjs +146 -26
  23. package/web-ui/modules/i18n/locales/zh.mjs +148 -28
  24. package/web-ui/modules/i18n.mjs +5 -12
  25. package/web-ui/modules/sessions-filters-url.mjs +1 -2
  26. package/web-ui/partials/index/layout-header.html +37 -14
  27. package/web-ui/partials/index/panel-orchestration.html +489 -282
  28. package/web-ui/res/web-ui-render.precompiled.js +1049 -610
  29. package/web-ui/styles/layout-shell.css +157 -1
  30. package/web-ui/styles/navigation-panels.css +11 -0
  31. package/web-ui/styles/task-orchestration.css +2161 -4
@@ -1,3 +1,5 @@
1
+ const OPENAI_CHAT_TRANSIENT_RETRY_LIMIT = 2;
2
+
1
3
  function isPlainObject(value) {
2
4
  return !!value && typeof value === 'object' && !Array.isArray(value);
3
5
  }
@@ -100,6 +102,23 @@ function buildTaskTitle(target, explicitTitle = '') {
100
102
  return truncateText(normalizedTarget.replace(/\s+/g, ' '), 96);
101
103
  }
102
104
 
105
+
106
+ function normalizeTaskEngine(value) {
107
+ const normalized = normalizeId(value, 'openai-chat');
108
+ if (normalized === 'workflow') return 'workflow';
109
+ // Legacy plans used the name `codex`; the execution base is now OpenAI Chat compatible.
110
+ return 'openai-chat';
111
+ }
112
+
113
+ function normalizeTaskNodeKind(value) {
114
+ const normalized = normalizeId(value, '');
115
+ if (!normalized) return '';
116
+ if (normalized === 'workflow') return 'workflow';
117
+ if (normalized === 'codex') return 'openai-chat';
118
+ if (normalized === 'openai' || normalized === 'openai-chat' || normalized === 'chat') return 'openai-chat';
119
+ return normalized;
120
+ }
121
+
103
122
  function normalizeDependencyIds(value = []) {
104
123
  return uniqueStringList(value)
105
124
  .map((depId) => normalizeId(depId, ''))
@@ -193,11 +212,15 @@ function detectDependencyCycle(nodes = []) {
193
212
  return false;
194
213
  }
195
214
 
196
- function buildCodexNodePrompt(kind, context = {}) {
215
+ const PLAN_DISCUSSION_PREFIX = '我们先探讨方案,在我让你生成之前不要生成';
216
+
217
+ function buildOpenAiChatNodePrompt(kind, context = {}) {
197
218
  const title = normalizeText(context.title, 160);
198
219
  const target = normalizeText(context.target, 4000);
199
220
  const item = normalizeText(context.item, 1200);
200
221
  const followUp = normalizeText(context.followUp, 1200);
222
+ const followUps = uniqueStringList(context.followUps || []).slice(0, 8);
223
+ const cwd = normalizeText(context.cwd, 1200);
201
224
  const dependencySummaries = uniqueStringList(context.dependencySummaries || []).slice(0, 6);
202
225
  const allowWrite = context.allowWrite === true;
203
226
  const dependencyBlock = dependencySummaries.length > 0
@@ -206,10 +229,12 @@ function buildCodexNodePrompt(kind, context = {}) {
206
229
  const writeRule = allowWrite
207
230
  ? '允许直接修改本地工作区,但必须控制变更范围并完成最小必要验证。'
208
231
  : '只允许只读调查,不要修改任何文件,不要执行写入型操作。';
232
+ const cwdLine = cwd ? `工作区路径: ${cwd}` : '';
209
233
 
210
234
  if (kind === 'analysis') {
211
235
  return [
212
236
  `任务标题: ${title || '任务分析'}`,
237
+ cwdLine,
213
238
  `任务目标:\n${target}`,
214
239
  writeRule,
215
240
  '请先调查当前仓库与上下文,给出简洁的现状判断、主要风险、建议执行顺序,以及你认为最小可行的验证方案。',
@@ -217,9 +242,25 @@ function buildCodexNodePrompt(kind, context = {}) {
217
242
  ].join('\n\n');
218
243
  }
219
244
 
245
+ if (kind === 'plan') {
246
+ const userDemand = [PLAN_DISCUSSION_PREFIX, target].filter(Boolean).join('\n\n');
247
+ const followUpBlock = followUps.length > 0
248
+ ? `\n补充需求:\n${followUps.map((entry, index) => `${index + 1}. ${entry}`).join('\n')}`
249
+ : '';
250
+ return [
251
+ `任务标题: ${title || '方案草拟'}`,
252
+ cwdLine,
253
+ `任务需求:\n${userDemand}`,
254
+ followUpBlock,
255
+ '请只输出可讨论、可确认的执行方案:目标拆解、建议步骤、风险点、需要用户确认的问题。',
256
+ '不要修改文件、不要启动实际执行,也不要把“等待用户确认”拆成依赖执行链。'
257
+ ].filter(Boolean).join('\n\n');
258
+ }
259
+
220
260
  if (kind === 'work') {
221
261
  return [
222
262
  `任务标题: ${title || '执行子任务'}`,
263
+ cwdLine,
223
264
  `总目标:\n${target}`,
224
265
  `当前子任务:\n${item || title || target}`,
225
266
  dependencyBlock,
@@ -231,6 +272,7 @@ function buildCodexNodePrompt(kind, context = {}) {
231
272
  if (kind === 'verify') {
232
273
  return [
233
274
  `任务标题: ${title || '验证与总结'}`,
275
+ cwdLine,
234
276
  `总目标:\n${target}`,
235
277
  dependencyBlock,
236
278
  writeRule,
@@ -240,6 +282,7 @@ function buildCodexNodePrompt(kind, context = {}) {
240
282
 
241
283
  return [
242
284
  `任务标题: ${title || '后续任务'}`,
285
+ cwdLine,
243
286
  `总目标:\n${target}`,
244
287
  dependencyBlock,
245
288
  writeRule,
@@ -267,7 +310,7 @@ function buildTaskPlan(request = {}, options = {}) {
267
310
  const workflowMap = normalizeWorkflowCatalog(options.workflowCatalog || []);
268
311
  const target = normalizeText(request.target, 4000);
269
312
  const title = buildTaskTitle(target, request.title);
270
- const engine = normalizeId(request.engine, 'codex') === 'workflow' ? 'workflow' : 'codex';
313
+ const requestedEngine = normalizeTaskEngine(request.engine);
271
314
  const allowWrite = request.allowWrite === true;
272
315
  const dryRun = request.dryRun === true;
273
316
  const concurrency = normalizePositiveInteger(request.concurrency, 2, 1, 8);
@@ -278,12 +321,33 @@ function buildTaskPlan(request = {}, options = {}) {
278
321
  const followUps = uniqueStringList(request.followUps || []);
279
322
  const notes = normalizeText(request.notes, 2000);
280
323
  const cwd = normalizeText(request.cwd || options.cwd, 1200);
324
+ const threadId = normalizeText(request.threadId || request.conversationId || request.sessionId, 160);
281
325
  const nodes = [];
282
326
  let nodeSequence = 0;
283
327
  const nextNodeId = (prefix) => `${prefix}-${String(++nodeSequence).padStart(2, '0')}`;
284
- const shouldBuildWorkflowNodes = requestedWorkflowIds.length > 0 || engine === 'workflow';
328
+ const shouldBuildWorkflowNodes = requestedWorkflowIds.length > 0 || requestedEngine === 'workflow';
329
+ const engine = shouldBuildWorkflowNodes ? 'workflow' : requestedEngine;
330
+ const previewOnly = request.previewOnly === true && !shouldBuildWorkflowNodes;
285
331
 
286
- if (shouldBuildWorkflowNodes) {
332
+ if (previewOnly) {
333
+ const previewId = nextNodeId('plan');
334
+ nodes.push({
335
+ id: previewId,
336
+ title: '方案草拟',
337
+ kind: 'openai-chat',
338
+ prompt: buildOpenAiChatNodePrompt('plan', {
339
+ title,
340
+ target,
341
+ followUps,
342
+ cwd,
343
+ allowWrite: false
344
+ }),
345
+ dependsOn: [],
346
+ write: false,
347
+ retryLimit: 0,
348
+ autoFixRounds: 0
349
+ });
350
+ } else if (shouldBuildWorkflowNodes) {
287
351
  let previousId = '';
288
352
  for (const workflowId of requestedWorkflowIds) {
289
353
  const meta = workflowMap.get(workflowId);
@@ -313,15 +377,16 @@ function buildTaskPlan(request = {}, options = {}) {
313
377
  nodes.push({
314
378
  id: analysisId,
315
379
  title: '现状分析',
316
- kind: 'codex',
317
- prompt: buildCodexNodePrompt('analysis', {
380
+ kind: 'openai-chat',
381
+ prompt: buildOpenAiChatNodePrompt('analysis', {
318
382
  title,
319
383
  target,
384
+ cwd,
320
385
  allowWrite: false
321
386
  }),
322
387
  dependsOn: [],
323
388
  write: false,
324
- retryLimit: 0,
389
+ retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT,
325
390
  autoFixRounds: 0
326
391
  });
327
392
 
@@ -333,16 +398,17 @@ function buildTaskPlan(request = {}, options = {}) {
333
398
  nodes.push({
334
399
  id: nodeId,
335
400
  title: truncateText(item, 72) || `执行 ${executionNodeIds.length}`,
336
- kind: 'codex',
337
- prompt: buildCodexNodePrompt('work', {
401
+ kind: 'openai-chat',
402
+ prompt: buildOpenAiChatNodePrompt('work', {
338
403
  title,
339
404
  target,
340
405
  item,
406
+ cwd,
341
407
  allowWrite
342
408
  }),
343
409
  dependsOn: [analysisId],
344
410
  write: allowWrite,
345
- retryLimit: 0,
411
+ retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT,
346
412
  autoFixRounds
347
413
  });
348
414
  }
@@ -351,15 +417,16 @@ function buildTaskPlan(request = {}, options = {}) {
351
417
  nodes.push({
352
418
  id: verifyId,
353
419
  title: '验证与总结',
354
- kind: 'codex',
355
- prompt: buildCodexNodePrompt('verify', {
420
+ kind: 'openai-chat',
421
+ prompt: buildOpenAiChatNodePrompt('verify', {
356
422
  title,
357
423
  target,
424
+ cwd,
358
425
  allowWrite: false
359
426
  }),
360
427
  dependsOn: executionNodeIds.slice(),
361
428
  write: false,
362
- retryLimit: 0,
429
+ retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT,
363
430
  autoFixRounds: 0
364
431
  });
365
432
  }
@@ -371,16 +438,17 @@ function buildTaskPlan(request = {}, options = {}) {
371
438
  nodes.push({
372
439
  id: nodeId,
373
440
  title: truncateText(followUp, 72) || `Follow-up ${nodes.length + 1}`,
374
- kind: 'codex',
375
- prompt: buildCodexNodePrompt('follow-up', {
441
+ kind: 'openai-chat',
442
+ prompt: buildOpenAiChatNodePrompt('follow-up', {
376
443
  title,
377
444
  target,
445
+ cwd,
378
446
  allowWrite,
379
447
  followUp
380
448
  }),
381
449
  dependsOn: followUpDependsOn,
382
450
  write: allowWrite,
383
- retryLimit: 0,
451
+ retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT,
384
452
  autoFixRounds
385
453
  });
386
454
  followUpDependsOn = [nodeId];
@@ -389,6 +457,7 @@ function buildTaskPlan(request = {}, options = {}) {
389
457
 
390
458
  const plan = {
391
459
  id: normalizeId(request.id, ''),
460
+ threadId,
392
461
  title,
393
462
  target,
394
463
  notes,
@@ -417,7 +486,7 @@ function validateTaskPlan(plan, options = {}) {
417
486
  }
418
487
 
419
488
  const workflowMap = normalizeWorkflowCatalog(options.workflowCatalog || []);
420
- const engine = normalizeId(plan.engine, 'codex') === 'workflow' ? 'workflow' : 'codex';
489
+ const engine = normalizeTaskEngine(plan.engine);
421
490
  const workflowIds = uniqueStringList(plan.workflowIds || []).map((id) => normalizeId(id, '')).filter(Boolean);
422
491
  const nodes = Array.isArray(plan.nodes) ? plan.nodes : [];
423
492
  if (engine === 'workflow' && workflowIds.length === 0) {
@@ -445,8 +514,8 @@ function validateTaskPlan(plan, options = {}) {
445
514
  continue;
446
515
  }
447
516
  nodeIds.add(id);
448
- const kind = normalizeId(node.kind, '');
449
- if (kind !== 'workflow' && kind !== 'codex') {
517
+ const kind = normalizeTaskNodeKind(node.kind);
518
+ if (kind !== 'workflow' && kind !== 'openai-chat') {
450
519
  issues.push({ code: 'task-node-kind-invalid', message: `task node ${id} has unsupported kind: ${node.kind || ''}` });
451
520
  }
452
521
  if (kind === 'workflow') {
@@ -457,7 +526,7 @@ function validateTaskPlan(plan, options = {}) {
457
526
  issues.push({ code: 'task-node-workflow-unknown', message: `task node ${id} references unknown workflow: ${workflowId}` });
458
527
  }
459
528
  }
460
- if (kind === 'codex' && !normalizeText(node.prompt, 12000)) {
529
+ if (kind === 'openai-chat' && !normalizeText(node.prompt, 12000)) {
461
530
  issues.push({ code: 'task-node-prompt-required', message: `task node ${id} missing prompt` });
462
531
  }
463
532
  }
@@ -492,7 +561,7 @@ function createNodeRunRecord(node, dependencyNodeIds = []) {
492
561
  return {
493
562
  id: normalizeId(node && node.id, ''),
494
563
  title: normalizeText(node && node.title, 160),
495
- kind: normalizeId(node && node.kind, ''),
564
+ kind: normalizeTaskNodeKind(node && node.kind),
496
565
  workflowId: normalizeId(node && node.workflowId, ''),
497
566
  dependsOn: normalizeDependencyIds(node && node.dependsOn),
498
567
  dependencyNodeIds: uniqueStringList(dependencyNodeIds),
@@ -0,0 +1,292 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const TEXT_FILE_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.md', '.txt', '.svg', '.ts', '.tsx', '.jsx', '.yml', '.yaml']);
7
+ const MAX_FILE_BYTES = 512 * 1024;
8
+ const MAX_CONTEXT_FILE_BYTES = 16 * 1024;
9
+ const MAX_CONTEXT_TOTAL_BYTES = 64 * 1024;
10
+ const MAX_HISTORY_MESSAGES = 20;
11
+ const SENSITIVE_CONTEXT_NAME_RE = /(^|[._\-/])(credential|credentials|secret|secrets|token|tokens|apikey|api-key|api_key|password|passwd|private-key|private_key|\.env)([._\-/]|$)/i;
12
+
13
+ function ensureDir(dir) {
14
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
15
+ }
16
+
17
+ function sanitizeThreadId(value) {
18
+ const text = typeof value === 'string' ? value.trim() : '';
19
+ if (!text) return '';
20
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(text) ? text : '';
21
+ }
22
+
23
+ function normalizeWorkspacePath(rawPath, cwd) {
24
+ const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd());
25
+ const text = typeof rawPath === 'string' ? rawPath.trim().replace(/^['"]|['"]$/g, '') : '';
26
+ if (!text || text.length > 240) {
27
+ return { error: 'workspace file path is empty or too long' };
28
+ }
29
+ if (path.isAbsolute(text) || text.includes('\\')) {
30
+ return { error: `workspace file path must be a safe relative path: ${text}` };
31
+ }
32
+ const normalized = path.normalize(text);
33
+ if (!normalized || normalized === '.' || normalized.startsWith('..') || normalized.split(path.sep).includes('..')) {
34
+ return { error: `workspace file path escapes cwd: ${text}` };
35
+ }
36
+ const ext = path.extname(normalized).toLowerCase();
37
+ if (!TEXT_FILE_EXTENSIONS.has(ext)) {
38
+ return { error: `workspace file extension is not allowed: ${ext || '(none)'}` };
39
+ }
40
+ const targetPath = path.resolve(baseDir, normalized);
41
+ const relative = path.relative(baseDir, targetPath);
42
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
43
+ return { error: `workspace file path escapes cwd: ${text}` };
44
+ }
45
+ return { path: targetPath, relativePath: normalized, baseDir };
46
+ }
47
+
48
+ function validateParentPath(targetPath, baseDir) {
49
+ const resolvedBase = fs.realpathSync.native(baseDir);
50
+ let current = path.dirname(targetPath);
51
+ const pending = [];
52
+ while (true) {
53
+ if (current === resolvedBase) return { ok: true };
54
+ if (current === path.dirname(current)) return { ok: false, error: 'workspace parent escapes cwd' };
55
+ if (fs.existsSync(current)) {
56
+ const stats = fs.lstatSync(current);
57
+ if (stats.isSymbolicLink()) return { ok: false, error: `workspace parent is a symlink: ${path.relative(baseDir, current) || current}` };
58
+ const resolvedCurrent = fs.realpathSync.native(current);
59
+ const relative = path.relative(resolvedBase, resolvedCurrent);
60
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return { ok: false, error: 'workspace parent resolves outside cwd' };
61
+ for (const child of pending) {
62
+ const childRelative = path.relative(resolvedBase, child);
63
+ if (childRelative.startsWith('..') || path.isAbsolute(childRelative)) return { ok: false, error: 'workspace parent escapes cwd' };
64
+ }
65
+ return { ok: true };
66
+ }
67
+ pending.push(current);
68
+ current = path.dirname(current);
69
+ }
70
+ }
71
+
72
+ function parseAttributes(info) {
73
+ const attrs = {};
74
+ const text = typeof info === 'string' ? info : '';
75
+ const attrRe = /([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g;
76
+ let match;
77
+ while ((match = attrRe.exec(text)) !== null) {
78
+ attrs[match[1]] = match[3] || match[4] || match[5] || '';
79
+ }
80
+ return attrs;
81
+ }
82
+
83
+ function parseWorkspaceFileOperations(text) {
84
+ const content = typeof text === 'string' ? text : '';
85
+ const operations = [];
86
+ const fenceRe = /```([^\n`]*)\n([\s\S]*?)```/g;
87
+ let match;
88
+ while ((match = fenceRe.exec(content)) !== null) {
89
+ const info = String(match[1] || '').trim();
90
+ if (!/^codexmate-file\b/i.test(info)) continue;
91
+ const attrs = parseAttributes(info);
92
+ const action = String(attrs.action || attrs.op || 'write').trim().toLowerCase();
93
+ const filePath = String(attrs.path || attrs.file || attrs.filename || '').trim();
94
+ operations.push({ action: action === 'update' ? 'write' : action, path: filePath, content: String(match[2] || '') });
95
+ }
96
+
97
+ const deleteRe = /^\s*CODEXMATE_(?:DELETE|REMOVE)_FILE\s*:\s*(.+?)\s*$/gmi;
98
+ while ((match = deleteRe.exec(content)) !== null) {
99
+ operations.push({ action: 'delete', path: String(match[1] || '').trim(), content: '' });
100
+ }
101
+ return operations;
102
+ }
103
+
104
+ function isSensitiveWorkspaceContextFile(relativePath) {
105
+ const normalized = String(relativePath || '').replace(/\\+/g, '/').toLowerCase();
106
+ return SENSITIVE_CONTEXT_NAME_RE.test(normalized);
107
+ }
108
+
109
+ function isWorkspaceFileMentioned(relativePath, promptText) {
110
+ const normalizedPath = String(relativePath || '').toLowerCase();
111
+ const baseName = path.basename(normalizedPath);
112
+ return !!normalizedPath && (promptText.includes(normalizedPath) || (!!baseName && promptText.includes(baseName)));
113
+ }
114
+
115
+ function applyWorkspaceFileOperations(text, cwd, options = {}) {
116
+ const allowWrite = options.allowWrite === true;
117
+ const operations = parseWorkspaceFileOperations(text);
118
+ const files = [];
119
+ const warnings = [];
120
+ if (!allowWrite && operations.length > 0) {
121
+ return { files, warnings: ['workspace file operations skipped because allowWrite is false'] };
122
+ }
123
+ for (const operation of operations) {
124
+ const action = operation.action === 'delete' ? 'delete' : 'write';
125
+ const normalized = normalizeWorkspacePath(operation.path, cwd);
126
+ if (normalized.error) {
127
+ warnings.push(normalized.error);
128
+ continue;
129
+ }
130
+ try {
131
+ if (action === 'delete') {
132
+ if (!fs.existsSync(normalized.path)) {
133
+ files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: 0, operation: 'delete', existed: false });
134
+ continue;
135
+ }
136
+ const stats = fs.lstatSync(normalized.path);
137
+ if (stats.isSymbolicLink()) {
138
+ warnings.push(`workspace target is a symlink: ${normalized.relativePath}`);
139
+ continue;
140
+ }
141
+ if (!stats.isFile()) {
142
+ warnings.push(`workspace target is not a file: ${normalized.relativePath}`);
143
+ continue;
144
+ }
145
+ fs.unlinkSync(normalized.path);
146
+ files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: 0, operation: 'delete', existed: true });
147
+ continue;
148
+ }
149
+
150
+ const body = String(operation.content || '');
151
+ const bytes = Buffer.byteLength(body, 'utf-8');
152
+ if (bytes > MAX_FILE_BYTES) {
153
+ warnings.push(`workspace artifact too large and skipped: ${normalized.relativePath}`);
154
+ continue;
155
+ }
156
+ const parentValidation = validateParentPath(normalized.path, normalized.baseDir);
157
+ if (!parentValidation.ok) {
158
+ warnings.push(parentValidation.error || `workspace parent is unsafe: ${normalized.relativePath}`);
159
+ continue;
160
+ }
161
+ ensureDir(path.dirname(normalized.path));
162
+ try {
163
+ if (fs.lstatSync(normalized.path).isSymbolicLink()) {
164
+ warnings.push(`workspace target is a symlink: ${normalized.relativePath}`);
165
+ continue;
166
+ }
167
+ } catch (error) {
168
+ if (!error || error.code !== 'ENOENT') throw error;
169
+ }
170
+ fs.writeFileSync(normalized.path, body, { encoding: 'utf-8', mode: 0o600 });
171
+ files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes, operation: 'write' });
172
+ } catch (error) {
173
+ warnings.push(`workspace operation failed for ${normalized.relativePath}: ${error && error.message ? error.message : String(error)}`);
174
+ }
175
+ }
176
+ return { files, warnings };
177
+ }
178
+
179
+ function walkWorkspace(cwd, options = {}) {
180
+ const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd());
181
+ const maxFiles = Number.isFinite(options.maxFiles) ? options.maxFiles : 120;
182
+ const files = [];
183
+ const skipNames = new Set(['.git', 'node_modules', '.codexmate', '.DS_Store']);
184
+ function walk(dir) {
185
+ if (files.length >= maxFiles) return;
186
+ let entries = [];
187
+ try {
188
+ entries = fs.readdirSync(dir, { withFileTypes: true });
189
+ } catch (_) {
190
+ return;
191
+ }
192
+ entries.sort((a, b) => a.name.localeCompare(b.name));
193
+ for (const entry of entries) {
194
+ if (files.length >= maxFiles) break;
195
+ if (skipNames.has(entry.name)) continue;
196
+ const fullPath = path.join(dir, entry.name);
197
+ let stats;
198
+ try {
199
+ stats = fs.lstatSync(fullPath);
200
+ } catch (_) {
201
+ continue;
202
+ }
203
+ if (stats.isSymbolicLink()) continue;
204
+ const relativePath = path.relative(baseDir, fullPath);
205
+ if (stats.isDirectory()) {
206
+ walk(fullPath);
207
+ } else if (stats.isFile()) {
208
+ files.push({ path: fullPath, relativePath, bytes: stats.size, ext: path.extname(relativePath).toLowerCase() });
209
+ }
210
+ }
211
+ }
212
+ if (fs.existsSync(baseDir)) walk(baseDir);
213
+ return files;
214
+ }
215
+
216
+ function buildWorkspaceChatContext(cwd, prompt = '', historyMessages = []) {
217
+ const files = walkWorkspace(cwd);
218
+ const promptText = [prompt, ...historyMessages.map((item) => item && item.content ? item.content : '')].join('\n').toLowerCase();
219
+ const treeLines = files.map((file) => `- ${file.relativePath} (${file.bytes} bytes)`);
220
+ const contentBlocks = [];
221
+ let total = 0;
222
+ for (const file of files) {
223
+ if (!TEXT_FILE_EXTENSIONS.has(file.ext)) continue;
224
+ if (file.bytes > MAX_CONTEXT_FILE_BYTES) continue;
225
+ const mentioned = isWorkspaceFileMentioned(file.relativePath, promptText);
226
+ if (!mentioned) continue;
227
+ if (isSensitiveWorkspaceContextFile(file.relativePath)) continue;
228
+ if (total + file.bytes > MAX_CONTEXT_TOTAL_BYTES) break;
229
+ try {
230
+ const body = fs.readFileSync(file.path, 'utf-8');
231
+ total += Buffer.byteLength(body, 'utf-8');
232
+ contentBlocks.push(`### ${file.relativePath}\n\`\`\`\n${body}\n\`\`\``);
233
+ } catch (_) {}
234
+ }
235
+ return [
236
+ '当前工作区文件列表:',
237
+ treeLines.length ? treeLines.join('\n') : '(empty workspace)',
238
+ contentBlocks.length ? '\n当前工作区可读文本文件内容快照:\n' + contentBlocks.join('\n\n') : '\n当前没有可注入的文本文件内容快照。'
239
+ ].join('\n');
240
+ }
241
+
242
+ function getThreadFile(storeDir, threadId) {
243
+ const safe = sanitizeThreadId(threadId);
244
+ if (!safe) return '';
245
+ return path.join(storeDir, `${safe}.json`);
246
+ }
247
+
248
+ function loadWorkspaceChatThread(storeDir, threadId) {
249
+ const file = getThreadFile(storeDir, threadId);
250
+ if (!file || !fs.existsSync(file)) return { threadId: sanitizeThreadId(threadId), messages: [] };
251
+ try {
252
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
253
+ const messages = Array.isArray(parsed.messages) ? parsed.messages.filter((item) => item && (item.role === 'user' || item.role === 'assistant') && typeof item.content === 'string') : [];
254
+ return { ...parsed, threadId: sanitizeThreadId(parsed.threadId || threadId), messages: messages.slice(-MAX_HISTORY_MESSAGES) };
255
+ } catch (_) {
256
+ return { threadId: sanitizeThreadId(threadId), messages: [] };
257
+ }
258
+ }
259
+
260
+ function appendWorkspaceChatThread(storeDir, threadId, messages, metadata = {}) {
261
+ const safe = sanitizeThreadId(threadId);
262
+ if (!safe) return { ok: false, error: 'invalid thread id' };
263
+ ensureDir(storeDir);
264
+ const existing = loadWorkspaceChatThread(storeDir, safe);
265
+ const now = new Date().toISOString();
266
+ const nextMessages = existing.messages.concat((Array.isArray(messages) ? messages : []).map((item) => ({
267
+ role: item.role === 'assistant' ? 'assistant' : 'user',
268
+ content: String(item.content || ''),
269
+ at: item.at || now
270
+ }))).slice(-MAX_HISTORY_MESSAGES);
271
+ const payload = {
272
+ threadId: safe,
273
+ cwd: metadata.cwd || existing.cwd || '',
274
+ updatedAt: now,
275
+ messages: nextMessages
276
+ };
277
+ const file = getThreadFile(storeDir, safe);
278
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2), { encoding: 'utf-8', mode: 0o600 });
279
+ return { ok: true, file, messageCount: nextMessages.length };
280
+ }
281
+
282
+ module.exports = {
283
+ TEXT_FILE_EXTENSIONS,
284
+ sanitizeThreadId,
285
+ normalizeWorkspacePath,
286
+ parseWorkspaceFileOperations,
287
+ applyWorkspaceFileOperations,
288
+ walkWorkspace,
289
+ buildWorkspaceChatContext,
290
+ loadWorkspaceChatThread,
291
+ appendWorkspaceChatThread
292
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.56",
3
+ "version": "0.0.57",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -72,7 +72,7 @@
72
72
  "author": "ymkiux",
73
73
  "license": "Apache-2.0",
74
74
  "devDependencies": {
75
- "@vue/compiler-dom": "^3.5.30",
75
+ "@vue/compiler-dom": "^3.5.34",
76
76
  "opencc-js": "^1.3.1",
77
77
  "vitepress": "^1.6.4"
78
78
  }