codexmate 0.0.55 → 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.
- package/README.md +2 -0
- package/README.vi.md +2 -0
- package/README.zh.md +2 -0
- package/cli/local-bridge.js +221 -22
- package/cli.js +797 -134
- package/lib/task-orchestrator.js +90 -21
- package/lib/task-workspace-chat.js +292 -0
- package/package.json +2 -2
- package/web-ui/app.js +57 -129
- package/web-ui/modules/app.computed.main-tabs.mjs +304 -7
- package/web-ui/modules/app.computed.session.mjs +210 -0
- package/web-ui/modules/app.methods.agents.mjs +3 -0
- package/web-ui/modules/app.methods.claude-config.mjs +178 -22
- package/web-ui/modules/app.methods.codex-config.mjs +294 -65
- package/web-ui/modules/app.methods.navigation.mjs +71 -84
- package/web-ui/modules/app.methods.openclaw-editing.mjs +3 -6
- package/web-ui/modules/app.methods.providers.mjs +15 -1
- package/web-ui/modules/app.methods.runtime.mjs +7 -2
- package/web-ui/modules/app.methods.session-actions.mjs +25 -12
- package/web-ui/modules/app.methods.session-browser.mjs +23 -54
- package/web-ui/modules/app.methods.session-trash.mjs +0 -1
- package/web-ui/modules/app.methods.startup-claude.mjs +35 -17
- package/web-ui/modules/app.methods.task-orchestration.mjs +347 -8
- package/web-ui/modules/app.methods.tool-config-permissions.mjs +0 -3
- package/web-ui/modules/app.methods.web-ui-preferences.mjs +415 -68
- package/web-ui/modules/config-template-confirm-pref.mjs +2 -3
- package/web-ui/modules/i18n/locales/en.mjs +187 -28
- package/web-ui/modules/i18n/locales/ja.mjs +184 -25
- package/web-ui/modules/i18n/locales/vi.mjs +186 -33
- package/web-ui/modules/i18n/locales/zh-tw.mjs +187 -28
- package/web-ui/modules/i18n/locales/zh.mjs +189 -30
- package/web-ui/modules/i18n.mjs +5 -12
- package/web-ui/modules/provider-default-names.mjs +25 -0
- package/web-ui/modules/sessions-filters-url.mjs +1 -2
- package/web-ui/partials/index/layout-header.html +37 -14
- package/web-ui/partials/index/modal-health-check.html +69 -5
- package/web-ui/partials/index/panel-config-codex.html +2 -2
- package/web-ui/partials/index/panel-orchestration.html +489 -282
- package/web-ui/partials/index/panel-sessions.html +97 -17
- package/web-ui/res/web-ui-render.precompiled.js +1423 -732
- package/web-ui/session-helpers.mjs +4 -1
- package/web-ui/styles/layout-shell.css +157 -1
- package/web-ui/styles/navigation-panels.css +11 -0
- package/web-ui/styles/responsive.css +98 -0
- package/web-ui/styles/sessions-preview.css +212 -2
- package/web-ui/styles/sessions-toolbar-trash.css +61 -18
- package/web-ui/styles/skills-list.css +122 -0
- package/web-ui/styles/task-orchestration.css +2161 -4
- package/web-ui/styles/titles-cards.css +52 -0
package/lib/task-orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 ||
|
|
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 (
|
|
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: '
|
|
317
|
-
prompt:
|
|
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:
|
|
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: '
|
|
337
|
-
prompt:
|
|
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:
|
|
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: '
|
|
355
|
-
prompt:
|
|
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:
|
|
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: '
|
|
375
|
-
prompt:
|
|
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:
|
|
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 =
|
|
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 =
|
|
449
|
-
if (kind !== 'workflow' && kind !== '
|
|
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 === '
|
|
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:
|
|
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.
|
|
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.
|
|
75
|
+
"@vue/compiler-dom": "^3.5.34",
|
|
76
76
|
"opencc-js": "^1.3.1",
|
|
77
77
|
"vitepress": "^1.6.4"
|
|
78
78
|
}
|