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
package/cli.js CHANGED
@@ -73,8 +73,15 @@ const {
73
73
  truncateText: truncateTaskText,
74
74
  buildTaskPlan,
75
75
  validateTaskPlan,
76
- executeTaskPlan
76
+ executeTaskPlan,
77
+ computePlanWaves
77
78
  } = require('./lib/task-orchestrator');
79
+ const {
80
+ buildWorkspaceChatContext,
81
+ loadWorkspaceChatThread,
82
+ appendWorkspaceChatThread,
83
+ applyWorkspaceFileOperations
84
+ } = require('./lib/task-workspace-chat');
78
85
  const {
79
86
  readAutomationConfig,
80
87
  matchAutomationRule,
@@ -258,6 +265,9 @@ const TASK_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-task-runs.jsonl');
258
265
  const TASK_RUN_DETAILS_DIR = path.join(CONFIG_DIR, 'codexmate-task-runs');
259
266
  const TASK_QUEUE_WORKER_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue-worker.json');
260
267
  const TASK_ARTIFACTS_DIR = path.join(CONFIG_DIR, 'codexmate-task-artifacts');
268
+ const TASK_WORKSPACE_CHAT_THREADS_DIR = path.join(CONFIG_DIR, 'codexmate-task-chat-threads');
269
+ const TASK_OPENAI_CHAT_TIMEOUT_MS = 180000;
270
+ const TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
261
271
  const AUTOMATION_CONFIG_FILE = path.join(CONFIG_DIR, 'codexmate-automation.json');
262
272
  const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
263
273
  const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
@@ -990,6 +1000,65 @@ function normalizePromptsSubTabPreference(value) {
990
1000
  return normalized === 'claude-project' ? 'claude-project' : 'codex';
991
1001
  }
992
1002
 
1003
+ function normalizeSidebarCollapsedPreference(value) {
1004
+ return normalizeBooleanPreference(value, false);
1005
+ }
1006
+
1007
+ function normalizeSessionFilterSourcePreference(value) {
1008
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
1009
+ if (normalized === 'all' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy') return normalized;
1010
+ return 'codex';
1011
+ }
1012
+
1013
+ function normalizeSessionRoleFilterPreference(value) {
1014
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
1015
+ if (normalized === 'user' || normalized === 'assistant' || normalized === 'system' || normalized === 'tool') return normalized;
1016
+ return 'all';
1017
+ }
1018
+
1019
+ function normalizeSessionTimePresetPreference(value) {
1020
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
1021
+ if (normalized === '24h' || normalized === '7d' || normalized === '30d') return normalized;
1022
+ return 'all';
1023
+ }
1024
+
1025
+ function normalizeSessionSortModePreference(value) {
1026
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
1027
+ return normalized === 'hot' ? 'hot' : 'time';
1028
+ }
1029
+
1030
+ function normalizeSessionFiltersPreference(value = {}) {
1031
+ const source = isPlainObject(value) ? value : {};
1032
+ return {
1033
+ source: normalizeSessionFilterSourcePreference(source.source),
1034
+ pathFilter: typeof source.pathFilter === 'string' ? source.pathFilter : '',
1035
+ query: typeof source.query === 'string' ? source.query : '',
1036
+ roleFilter: normalizeSessionRoleFilterPreference(source.roleFilter),
1037
+ timePreset: normalizeSessionTimePresetPreference(source.timePreset),
1038
+ sortMode: normalizeSessionSortModePreference(source.sortMode)
1039
+ };
1040
+ }
1041
+
1042
+ function normalizeSessionPinnedMapPreference(value = {}) {
1043
+ const source = isPlainObject(value) ? value : {};
1044
+ const next = {};
1045
+ for (const [key, item] of Object.entries(source)) {
1046
+ if (!key) continue;
1047
+ const numeric = Number(item);
1048
+ if (!Number.isFinite(numeric) || numeric <= 0) continue;
1049
+ next[key] = Math.floor(numeric);
1050
+ }
1051
+ return next;
1052
+ }
1053
+
1054
+ function normalizePlainObjectPreference(value = {}) {
1055
+ return isPlainObject(value) ? value : {};
1056
+ }
1057
+
1058
+ function normalizeArrayPreference(value = []) {
1059
+ return Array.isArray(value) ? value : [];
1060
+ }
1061
+
993
1062
  function normalizeWebUiPreferences(value = {}) {
994
1063
  const source = isPlainObject(value) ? value : {};
995
1064
  const navigation = isPlainObject(source.navigation) ? source.navigation : {};
@@ -1002,6 +1071,18 @@ function normalizeWebUiPreferences(value = {}) {
1002
1071
  sessionsUsageTimeRange: normalizeUsageTimeRangePreference(source.sessionsUsageTimeRange),
1003
1072
  promptsSubTab: normalizePromptsSubTabPreference(source.promptsSubTab),
1004
1073
  projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
1074
+ sidebarCollapsed: normalizeSidebarCollapsedPreference(source.sidebarCollapsed),
1075
+ starPrompted: normalizeBooleanPreference(source.starPrompted, false),
1076
+ taskOrchestrationTabEnabled: normalizeBooleanPreference(source.taskOrchestrationTabEnabled, true),
1077
+ sessionLoadNativeDialog: normalizeBooleanPreference(source.sessionLoadNativeDialog, false),
1078
+ language: typeof source.language === 'string' ? source.language : '',
1079
+ sessionFilters: normalizeSessionFiltersPreference(source.sessionFilters),
1080
+ sessionPinnedMap: normalizeSessionPinnedMapPreference(source.sessionPinnedMap),
1081
+ claudeConfigs: normalizePlainObjectPreference(source.claudeConfigs),
1082
+ currentClaudeConfig: typeof source.currentClaudeConfig === 'string' ? source.currentClaudeConfig : '',
1083
+ openclawConfigs: normalizePlainObjectPreference(source.openclawConfigs),
1084
+ toolConfigPermissions: normalizeToolConfigPermissions(source.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS),
1085
+ deletedClaudeSettingsImports: normalizeArrayPreference(source.deletedClaudeSettingsImports),
1005
1086
  navigation: {
1006
1087
  mainTab: normalizeMainTabPreference(navigation.mainTab),
1007
1088
  configMode: normalizeConfigModePreference(navigation.configMode),
@@ -1030,6 +1111,9 @@ function setWebUiPreferences(params = {}) {
1030
1111
  }
1031
1112
  });
1032
1113
  preferences.webUi = next;
1114
+ if (isPlainObject(incoming.toolConfigPermissions)) {
1115
+ preferences.toolConfigPermissions = normalizeToolConfigPermissions(incoming.toolConfigPermissions);
1116
+ }
1033
1117
  writeCodexmatePreferences(preferences);
1034
1118
  return { success: true, preferences: next };
1035
1119
  }
@@ -13015,6 +13099,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13015
13099
  detached: true,
13016
13100
  taskId,
13017
13101
  runId,
13102
+ threadId: plan.threadId || '',
13103
+ cwd: plan.cwd || '',
13018
13104
  warnings: validation.warnings || []
13019
13105
  };
13020
13106
  } else {
@@ -13859,7 +13945,11 @@ function printTaskHelp() {
13859
13945
  console.log(' --allow-write 允许写入工作区');
13860
13946
  console.log(' --dry-run 仅计划/预演,不执行写入');
13861
13947
  console.log(' --plan-only 仅输出计划,不执行');
13862
- console.log(' --engine <codex|workflow> 选择编排引擎');
13948
+ console.log(' --engine <openai-chat|workflow> 选择编排引擎');
13949
+ console.log(' --cwd <路径> 指定任务工作区路径');
13950
+ console.log(' --thread-id <ID> 指定任务线程 ID');
13951
+ console.log(' --conversation-id <ID> 指定任务线程 ID(兼容别名)');
13952
+ console.log(' --session-id <ID> 指定任务线程 ID(兼容别名)');
13863
13953
  console.log(' --concurrency <N> 并发度');
13864
13954
  console.log(' --auto-fix-rounds <N> 自动修复回合数');
13865
13955
  console.log(' --limit <N> runs/queue list 数量');
@@ -13882,7 +13972,7 @@ function parseTaskCliOptions(args = []) {
13882
13972
  allowWrite: false,
13883
13973
  dryRun: false,
13884
13974
  planOnly: false,
13885
- engine: 'codex',
13975
+ engine: 'openai-chat',
13886
13976
  concurrency: 2,
13887
13977
  autoFixRounds: 1,
13888
13978
  limit: 20,
@@ -14029,6 +14119,38 @@ function parseTaskCliOptions(args = []) {
14029
14119
  options.explicit.autoFixRounds = true;
14030
14120
  continue;
14031
14121
  }
14122
+ if (arg === '--cwd') {
14123
+ options.cwd = String(args[i + 1] || '').trim();
14124
+ options.explicit.cwd = true;
14125
+ i += 1;
14126
+ continue;
14127
+ }
14128
+ if (arg.startsWith('--cwd=')) {
14129
+ options.cwd = arg.slice('--cwd='.length).trim();
14130
+ options.explicit.cwd = true;
14131
+ continue;
14132
+ }
14133
+ if (arg === '--thread-id' || arg === '--conversation-id' || arg === '--session-id') {
14134
+ options.threadId = String(args[i + 1] || '').trim();
14135
+ options.explicit.threadId = true;
14136
+ i += 1;
14137
+ continue;
14138
+ }
14139
+ if (arg.startsWith('--thread-id=')) {
14140
+ options.threadId = arg.slice('--thread-id='.length).trim();
14141
+ options.explicit.threadId = true;
14142
+ continue;
14143
+ }
14144
+ if (arg.startsWith('--conversation-id=')) {
14145
+ options.threadId = arg.slice('--conversation-id='.length).trim();
14146
+ options.explicit.threadId = true;
14147
+ continue;
14148
+ }
14149
+ if (arg.startsWith('--session-id=')) {
14150
+ options.threadId = arg.slice('--session-id='.length).trim();
14151
+ options.explicit.threadId = true;
14152
+ continue;
14153
+ }
14032
14154
  if (arg === '--limit') {
14033
14155
  const value = parseInt(args[i + 1], 10);
14034
14156
  if (Number.isFinite(value)) options.limit = value;
@@ -14086,9 +14208,11 @@ function buildTaskCliPayload(options = {}, rest = []) {
14086
14208
  if (explicit.followUps && Array.isArray(options.followUps)) payload.followUps = options.followUps.slice();
14087
14209
  if (explicit.allowWrite) payload.allowWrite = options.allowWrite === true;
14088
14210
  if (explicit.dryRun) payload.dryRun = options.dryRun === true;
14089
- if (explicit.engine) payload.engine = options.engine || 'codex';
14211
+ if (explicit.engine) payload.engine = options.engine || 'openai-chat';
14090
14212
  if (explicit.concurrency) payload.concurrency = options.concurrency;
14091
14213
  if (explicit.autoFixRounds) payload.autoFixRounds = options.autoFixRounds;
14214
+ if (explicit.cwd && options.cwd) payload.cwd = options.cwd;
14215
+ if (explicit.threadId && options.threadId) payload.threadId = options.threadId;
14092
14216
  if (explicit.taskId && options.taskId) payload.taskId = options.taskId;
14093
14217
  if (explicit.runId && options.runId) payload.runId = options.runId;
14094
14218
  if (!payload.target && Array.isArray(rest) && rest.length > 0) {
@@ -14102,7 +14226,7 @@ function buildTaskCliPayload(options = {}, rest = []) {
14102
14226
 
14103
14227
  function printTaskPlanSummary(plan, warnings = []) {
14104
14228
  console.log(`\n任务计划: ${plan.title || '(untitled)'}`);
14105
- console.log(` engine: ${plan.engine || 'codex'}`);
14229
+ console.log(` engine: ${plan.engine || 'openai-chat'}`);
14106
14230
  console.log(` allowWrite: ${plan.allowWrite === true ? 'yes' : 'no'}`);
14107
14231
  console.log(` dryRun: ${plan.dryRun === true ? 'yes' : 'no'}`);
14108
14232
  console.log(` concurrency: ${plan.concurrency || 1}`);
@@ -15556,6 +15680,10 @@ function createTaskRunId() {
15556
15680
  return `tr-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
15557
15681
  }
15558
15682
 
15683
+ function createTaskThreadId() {
15684
+ return `tt-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
15685
+ }
15686
+
15559
15687
  function validateTaskRunId(value) {
15560
15688
  const runId = typeof value === 'string' ? value.trim() : '';
15561
15689
  if (!runId) {
@@ -15567,9 +15695,19 @@ function validateTaskRunId(value) {
15567
15695
  return { ok: true, error: '', runId };
15568
15696
  }
15569
15697
 
15698
+ function normalizeTaskThreadId(value) {
15699
+ const threadId = typeof value === 'string' ? value.trim() : '';
15700
+ if (!threadId) {
15701
+ return '';
15702
+ }
15703
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(threadId) ? threadId : '';
15704
+ }
15705
+
15570
15706
  function normalizeTaskEngine(value) {
15571
- const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
15572
- return normalized === 'workflow' ? 'workflow' : 'codex';
15707
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s_/]+/g, '-') : '';
15708
+ if (normalized === 'workflow') return 'workflow';
15709
+ // Legacy task plans used `codex`; new task execution uses the configured OpenAI Chat-compatible provider.
15710
+ return 'openai-chat';
15573
15711
  }
15574
15712
 
15575
15713
  function normalizeTaskFollowUps(input = []) {
@@ -15610,6 +15748,7 @@ function normalizeTaskPlanRequest(params = {}) {
15610
15748
  : (typeof source.followUp === 'string' && source.followUp.trim() ? [source.followUp.trim()] : []);
15611
15749
  return {
15612
15750
  id: typeof source.id === 'string' ? source.id.trim() : '',
15751
+ threadId: normalizeTaskThreadId(source.threadId || source.conversationId || source.sessionId),
15613
15752
  title: typeof source.title === 'string' ? source.title.trim() : '',
15614
15753
  target: typeof source.target === 'string' ? source.target.trim() : '',
15615
15754
  notes: typeof source.notes === 'string' ? source.notes.trim() : '',
@@ -15619,6 +15758,7 @@ function normalizeTaskPlanRequest(params = {}) {
15619
15758
  dryRun: source.dryRun === true,
15620
15759
  concurrency: Number.isFinite(source.concurrency) ? source.concurrency : parseInt(source.concurrency, 10),
15621
15760
  autoFixRounds: Number.isFinite(source.autoFixRounds) ? source.autoFixRounds : parseInt(source.autoFixRounds, 10),
15761
+ previewOnly: source.previewOnly === true,
15622
15762
  workflowIds: rawWorkflowIds,
15623
15763
  followUps: normalizeTaskFollowUps(rawFollowUps)
15624
15764
  };
@@ -15627,12 +15767,18 @@ function normalizeTaskPlanRequest(params = {}) {
15627
15767
  function coerceTaskPlanPayload(params = {}) {
15628
15768
  if (params && params.plan && typeof params.plan === 'object' && !Array.isArray(params.plan)) {
15629
15769
  const plan = cloneJson(params.plan, {});
15630
- const overrideKeys = ['id', 'title', 'target', 'notes', 'cwd', 'engine', 'allowWrite', 'dryRun', 'concurrency', 'autoFixRounds', 'workflowIds', 'followUps'];
15770
+ const overrideKeys = ['id', 'threadId', 'conversationId', 'sessionId', 'title', 'target', 'notes', 'cwd', 'engine', 'allowWrite', 'dryRun', 'concurrency', 'autoFixRounds', 'previewOnly', 'workflowIds', 'followUps'];
15631
15771
  for (const key of overrideKeys) {
15632
15772
  if (Object.prototype.hasOwnProperty.call(params, key) && params[key] !== undefined) {
15633
- plan[key] = cloneJson(params[key], params[key]);
15773
+ if (key === 'conversationId' || key === 'sessionId') {
15774
+ plan.threadId = cloneJson(params[key], params[key]);
15775
+ } else {
15776
+ plan[key] = cloneJson(params[key], params[key]);
15777
+ }
15634
15778
  }
15635
15779
  }
15780
+ plan.cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd();
15781
+ plan.threadId = normalizeTaskThreadId(plan.threadId) || createTaskThreadId();
15636
15782
  plan.engine = normalizeTaskEngine(plan.engine);
15637
15783
  plan.workflowIds = normalizeTaskFollowUps(plan.workflowIds || []).map((id) => normalizeWorkflowId(id)).filter(Boolean);
15638
15784
  plan.followUps = normalizeTaskFollowUps(plan.followUps || []);
@@ -15647,6 +15793,7 @@ function coerceTaskPlanPayload(params = {}) {
15647
15793
  });
15648
15794
  return {
15649
15795
  ...plan,
15796
+ threadId: request.threadId || createTaskThreadId(),
15650
15797
  engine: normalizeTaskEngine(request.engine || plan.engine)
15651
15798
  };
15652
15799
  }
@@ -15669,8 +15816,10 @@ function normalizeTaskQueueItem(raw = {}) {
15669
15816
  const taskId = typeof raw.taskId === 'string' ? raw.taskId.trim() : '';
15670
15817
  return {
15671
15818
  taskId: taskId || createTaskId(),
15819
+ threadId: normalizeTaskThreadId(raw.threadId || plan.threadId) || createTaskThreadId(),
15672
15820
  title: typeof raw.title === 'string' ? raw.title.trim() : (typeof plan.title === 'string' ? plan.title.trim() : ''),
15673
15821
  target: typeof raw.target === 'string' ? raw.target.trim() : (typeof plan.target === 'string' ? plan.target.trim() : ''),
15822
+ cwd: typeof raw.cwd === 'string' && raw.cwd.trim() ? raw.cwd.trim() : (typeof plan.cwd === 'string' ? plan.cwd.trim() : ''),
15674
15823
  status: typeof raw.status === 'string' ? raw.status.trim().toLowerCase() : 'queued',
15675
15824
  createdAt: toIsoTime(raw.createdAt || Date.now(), ''),
15676
15825
  updatedAt: toIsoTime(raw.updatedAt || raw.createdAt || Date.now(), ''),
@@ -15871,8 +16020,10 @@ function collectTaskRunSummary(detail = {}) {
15871
16020
  return {
15872
16021
  runId: detail.runId || '',
15873
16022
  taskId: detail.taskId || '',
16023
+ threadId: detail.threadId || '',
15874
16024
  title: detail.title || '',
15875
16025
  target: detail.target || '',
16026
+ cwd: detail.cwd || '',
15876
16027
  engine: detail.engine || '',
15877
16028
  allowWrite: detail.allowWrite === true,
15878
16029
  dryRun: detail.dryRun === true,
@@ -15996,6 +16147,7 @@ function buildTaskOverviewPayload(options = {}) {
15996
16147
  }
15997
16148
  return {
15998
16149
  workflows: workflowCatalog.workflows,
16150
+ openAiChatStatus: buildTaskOpenAiChatStatus(),
15999
16151
  warnings,
16000
16152
  queue,
16001
16153
  runs,
@@ -16058,20 +16210,434 @@ function readCodexLastMessageFile(filePath) {
16058
16210
  }
16059
16211
  }
16060
16212
 
16061
- async function runCodexExecTaskNode(node, context = {}) {
16062
- const codexPath = resolveSpawnCommand('codex');
16063
- const codexProbeCommand = process.platform === 'win32' ? 'codex' : codexPath;
16064
- if (!commandExists(codexProbeCommand, '--version')) {
16213
+ function buildOpenAiChatEndpointUrl(baseUrl) {
16214
+ const trimmed = normalizeBaseUrl(baseUrl);
16215
+ if (!trimmed) return '';
16216
+ if (/\/v1\/chat\/completions$/i.test(trimmed) || /\/chat\/completions$/i.test(trimmed)) {
16217
+ return trimmed;
16218
+ }
16219
+ return joinApiUrl(trimmed, 'chat/completions');
16220
+ }
16221
+
16222
+ function redactTaskEndpointUrl(endpointUrl = '') {
16223
+ const raw = String(endpointUrl || '').trim();
16224
+ if (!raw) return '';
16225
+ try {
16226
+ const parsed = new URL(raw);
16227
+ if (parsed.username) parsed.username = '***';
16228
+ if (parsed.password) parsed.password = '***';
16229
+ const secretKeyPattern = /(?:^|[_-])(?:key|api[-_]?key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|credential|credentials|signature|sig)(?:$|[_-])/i;
16230
+ for (const key of [...parsed.searchParams.keys()]) {
16231
+ if (secretKeyPattern.test(key)) {
16232
+ parsed.searchParams.set(key, '***');
16233
+ }
16234
+ }
16235
+ return parsed.toString();
16236
+ } catch (_) {
16237
+ return raw
16238
+ .replace(/\/\/([^/@:]+):([^/@]+)@/g, '//***:***@')
16239
+ .replace(/([?&][^=&]*(?:key|token|secret|password|passwd|pwd|credential|signature|sig)[^=]*=)[^&]*/ig, '$1***');
16240
+ }
16241
+ }
16242
+
16243
+ function pickTaskProviderModel(providerName, provider, config) {
16244
+ const currentModels = readCurrentModels();
16245
+ const savedModel = currentModels && typeof currentModels[providerName] === 'string'
16246
+ ? currentModels[providerName].trim()
16247
+ : '';
16248
+ const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
16249
+ const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
16250
+ const providerModels = Array.isArray(provider && provider.models) ? provider.models : [];
16251
+ const firstProviderModel = providerModels
16252
+ .map((item) => {
16253
+ if (typeof item === 'string') return item.trim();
16254
+ if (item && typeof item === 'object') return String(item.id || item.name || item.model || '').trim();
16255
+ return '';
16256
+ })
16257
+ .find(Boolean) || '';
16258
+ return savedModel || (activeProvider === providerName ? activeModel : '') || firstProviderModel;
16259
+ }
16260
+
16261
+ function pickTaskProviderTemperature(provider) {
16262
+ const raw = provider && Object.prototype.hasOwnProperty.call(provider, 'temperature')
16263
+ ? provider.temperature
16264
+ : undefined;
16265
+ if (raw === undefined || raw === null || String(raw).trim() === '') {
16266
+ return 0.2;
16267
+ }
16268
+ const value = Number(raw);
16269
+ if (!Number.isFinite(value) || value < 0 || value > 2) {
16270
+ return 0.2;
16271
+ }
16272
+ return value;
16273
+ }
16274
+
16275
+ function pickTaskOpenAiChatProviderName(config) {
16276
+ const taskProvider = typeof config.task_openai_chat_provider === 'string'
16277
+ ? config.task_openai_chat_provider.trim()
16278
+ : '';
16279
+ if (taskProvider) {
16280
+ return taskProvider;
16281
+ }
16282
+ return typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
16283
+ }
16284
+
16285
+ function resolveTaskOpenAiChatConfig() {
16286
+ const configResult = readConfigOrVirtualDefault();
16287
+ const config = configResult && configResult.config && typeof configResult.config === 'object' ? configResult.config : {};
16288
+ const providerName = pickTaskOpenAiChatProviderName(config);
16289
+ if (!providerName) {
16290
+ return { error: '未设置当前 OpenAI Chat 提供商' };
16291
+ }
16292
+ const providers = config.model_providers && typeof config.model_providers === 'object' ? config.model_providers : {};
16293
+ const provider = providers[providerName];
16294
+ if (!provider || typeof provider !== 'object') {
16295
+ return { error: `OpenAI Chat 提供商不存在: ${providerName}` };
16296
+ }
16297
+
16298
+ const bridgeType = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
16299
+ const isOpenaiBridgeProvider = bridgeType === 'openai'
16300
+ || (typeof provider.base_url === 'string' && provider.base_url.includes('/bridge/openai/'));
16301
+ let baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
16302
+ let apiKey = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
16303
+ let extraHeaders = {};
16304
+ if (isOpenaiBridgeProvider) {
16305
+ const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, providerName);
16306
+ if (upstream && !upstream.error) {
16307
+ baseUrl = upstream.baseUrl || baseUrl;
16308
+ apiKey = upstream.apiKey || apiKey;
16309
+ extraHeaders = upstream.headers && typeof upstream.headers === 'object' && !Array.isArray(upstream.headers)
16310
+ ? upstream.headers
16311
+ : {};
16312
+ }
16313
+ }
16314
+
16315
+ const model = pickTaskProviderModel(providerName, provider, config);
16316
+ if (!baseUrl) {
16317
+ return { error: `OpenAI Chat 提供商 ${providerName} 缺少 base_url` };
16318
+ }
16319
+ if (!isValidHttpUrl(baseUrl)) {
16320
+ return { error: `OpenAI Chat 提供商 ${providerName} 的 base_url 无效` };
16321
+ }
16322
+ if (!model) {
16323
+ return { error: `OpenAI Chat 提供商 ${providerName} 未设置模型` };
16324
+ }
16325
+ return {
16326
+ providerName,
16327
+ baseUrl,
16328
+ endpointUrl: buildOpenAiChatEndpointUrl(baseUrl),
16329
+ apiKey,
16330
+ extraHeaders,
16331
+ model,
16332
+ temperature: pickTaskProviderTemperature(provider)
16333
+ };
16334
+ }
16335
+
16336
+ function buildTaskOpenAiChatStatus() {
16337
+ const requestConfig = resolveTaskOpenAiChatConfig();
16338
+ if (requestConfig && requestConfig.error) {
16339
+ return {
16340
+ ok: false,
16341
+ ready: false,
16342
+ error: requestConfig.error,
16343
+ providerName: '',
16344
+ model: '',
16345
+ endpoint: '',
16346
+ hasApiKey: false,
16347
+ hasExtraHeaders: false
16348
+ };
16349
+ }
16350
+ const hasApiKey = !!(requestConfig && typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim());
16351
+ const hasExtraHeaders = !!(requestConfig
16352
+ && requestConfig.extraHeaders
16353
+ && typeof requestConfig.extraHeaders === 'object'
16354
+ && !Array.isArray(requestConfig.extraHeaders)
16355
+ && Object.keys(requestConfig.extraHeaders).length > 0);
16356
+ const hasAuthMaterial = hasApiKey || hasExtraHeaders;
16357
+ return {
16358
+ ok: true,
16359
+ ready: hasAuthMaterial,
16360
+ error: hasAuthMaterial ? '' : `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`,
16361
+ providerName: requestConfig.providerName || '',
16362
+ model: requestConfig.model || '',
16363
+ endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''),
16364
+ hasApiKey,
16365
+ hasExtraHeaders
16366
+ };
16367
+ }
16368
+
16369
+ function postOpenAiChatCompletion(requestConfig, body, options = {}) {
16370
+ const endpointUrl = requestConfig && requestConfig.endpointUrl ? requestConfig.endpointUrl : '';
16371
+ return new Promise((resolve) => {
16372
+ let parsed;
16373
+ try {
16374
+ parsed = new URL(endpointUrl);
16375
+ } catch (error) {
16376
+ resolve({ ok: false, error: 'OpenAI Chat endpoint URL 无效', status: 0, payload: null, body: '' });
16377
+ return;
16378
+ }
16379
+ const transport = parsed.protocol === 'https:' ? https : http;
16380
+ const agent = parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT;
16381
+ const payloadText = JSON.stringify(body || {});
16382
+ const headers = {
16383
+ 'Content-Type': 'application/json',
16384
+ 'Accept': 'application/json',
16385
+ 'User-Agent': 'codexmate-task-orchestration',
16386
+ 'Content-Length': Buffer.byteLength(payloadText, 'utf-8'),
16387
+ ...(requestConfig.extraHeaders && typeof requestConfig.extraHeaders === 'object' ? requestConfig.extraHeaders : {})
16388
+ };
16389
+ if (requestConfig.apiKey) {
16390
+ headers.Authorization = `Bearer ${requestConfig.apiKey}`;
16391
+ }
16392
+ let settled = false;
16393
+ let req = null;
16394
+ const finish = (result) => {
16395
+ if (settled) return;
16396
+ settled = true;
16397
+ resolve(result);
16398
+ };
16399
+ req = transport.request(parsed, { method: 'POST', headers, agent }, (res) => {
16400
+ const status = res.statusCode || 0;
16401
+ let raw = '';
16402
+ let receivedBytes = 0;
16403
+ res.on('data', (chunk) => {
16404
+ if (settled) return;
16405
+ receivedBytes += chunk.length || 0;
16406
+ if (receivedBytes > TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES) {
16407
+ finish({ ok: false, status, error: 'OpenAI Chat response too large', payload: null, body: raw });
16408
+ res.destroy();
16409
+ return;
16410
+ }
16411
+ raw += chunk;
16412
+ });
16413
+ res.on('error', (error) => {
16414
+ finish({
16415
+ ok: false,
16416
+ status,
16417
+ error: error && error.message ? error.message : 'OpenAI Chat request failed',
16418
+ payload: null,
16419
+ body: raw
16420
+ });
16421
+ });
16422
+ res.on('end', () => {
16423
+ if (settled) return;
16424
+ let parsedPayload = null;
16425
+ try {
16426
+ parsedPayload = raw ? JSON.parse(raw) : null;
16427
+ } catch (_) {}
16428
+ if (status < 200 || status >= 300) {
16429
+ const message = parsedPayload && parsedPayload.error
16430
+ ? (typeof parsedPayload.error === 'string' ? parsedPayload.error : (parsedPayload.error.message || JSON.stringify(parsedPayload.error)))
16431
+ : (raw || `OpenAI Chat request failed: ${status}`);
16432
+ finish({ ok: false, status, error: truncateTaskText(message, 1200), payload: parsedPayload, body: raw });
16433
+ return;
16434
+ }
16435
+ finish({ ok: true, status, error: '', payload: parsedPayload, body: raw });
16436
+ });
16437
+ });
16438
+ if (typeof options.registerAbort === 'function') {
16439
+ options.registerAbort(() => {
16440
+ try {
16441
+ req.destroy(new Error('cancelled'));
16442
+ } catch (_) {}
16443
+ });
16444
+ }
16445
+ req.setTimeout(TASK_OPENAI_CHAT_TIMEOUT_MS, () => {
16446
+ req.destroy(new Error('OpenAI Chat request timeout'));
16447
+ });
16448
+ req.on('error', (error) => {
16449
+ finish({ ok: false, status: 0, error: error && error.message ? error.message : String(error || 'OpenAI Chat request failed'), payload: null, body: '' });
16450
+ });
16451
+ req.write(payloadText);
16452
+ req.end();
16453
+ });
16454
+ }
16455
+
16456
+ const TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES = 512 * 1024;
16457
+ const TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.md', '.txt', '.svg']);
16458
+
16459
+ function normalizeTaskMaterializedArtifactPath(rawPath, cwd) {
16460
+ const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd());
16461
+ const text = typeof rawPath === 'string' ? rawPath.trim() : '';
16462
+ if (!text || text.length > 200) {
16463
+ return { error: 'artifact path is empty or too long' };
16464
+ }
16465
+ if (path.isAbsolute(text) || text.includes('\\')) {
16466
+ return { error: `artifact path must be a safe relative path: ${text}` };
16467
+ }
16468
+ const normalized = path.normalize(text);
16469
+ if (!normalized || normalized === '.' || normalized.startsWith('..') || normalized.split(path.sep).includes('..')) {
16470
+ return { error: `artifact path escapes cwd: ${text}` };
16471
+ }
16472
+ const ext = path.extname(normalized).toLowerCase();
16473
+ if (!TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS.has(ext)) {
16474
+ return { error: `artifact extension is not allowed: ${ext || '(none)'}` };
16475
+ }
16476
+ const targetPath = path.resolve(baseDir, normalized);
16477
+ const relative = path.relative(baseDir, targetPath);
16478
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
16479
+ return { error: `artifact path escapes cwd: ${text}` };
16480
+ }
16481
+ return { path: targetPath, relativePath: normalized, baseDir };
16482
+ }
16483
+
16484
+ function validateTaskMaterializedArtifactParents(targetPath, baseDir) {
16485
+ const resolvedBase = fs.realpathSync.native(baseDir);
16486
+ let current = path.dirname(targetPath);
16487
+ const pending = [];
16488
+ while (true) {
16489
+ if (current === resolvedBase) {
16490
+ return { ok: true };
16491
+ }
16492
+ if (current === path.dirname(current)) {
16493
+ return { ok: false, error: 'artifact parent escapes cwd' };
16494
+ }
16495
+ if (fs.existsSync(current)) {
16496
+ const stats = fs.lstatSync(current);
16497
+ if (stats.isSymbolicLink()) {
16498
+ return { ok: false, error: `artifact parent is a symlink: ${path.relative(baseDir, current) || current}` };
16499
+ }
16500
+ const resolvedCurrent = fs.realpathSync.native(current);
16501
+ const relative = path.relative(resolvedBase, resolvedCurrent);
16502
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
16503
+ return { ok: false, error: 'artifact parent resolves outside cwd' };
16504
+ }
16505
+ for (const child of pending) {
16506
+ const childRelative = path.relative(resolvedBase, child);
16507
+ if (childRelative.startsWith('..') || path.isAbsolute(childRelative)) {
16508
+ return { ok: false, error: 'artifact parent escapes cwd' };
16509
+ }
16510
+ }
16511
+ return { ok: true };
16512
+ }
16513
+ pending.push(current);
16514
+ current = path.dirname(current);
16515
+ }
16516
+ }
16517
+
16518
+ function inferTaskMaterializedArtifactPath(info, prefix, hints) {
16519
+ const sources = [info, prefix, hints].map((item) => String(item || '')).filter(Boolean);
16520
+ const explicitPatterns = [
16521
+ /(?:file|filename|path)\s*[:=]\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i,
16522
+ /(?:文件|路径|保存为|写入到?|输出到?)\s*[::]?\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i,
16523
+ /[`"']([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']/i
16524
+ ];
16525
+ for (const source of sources) {
16526
+ for (const pattern of explicitPatterns) {
16527
+ const match = source.match(pattern);
16528
+ if (match && match[1]) {
16529
+ return match[1];
16530
+ }
16531
+ }
16532
+ }
16533
+ const normalizedInfo = String(info || '').trim().toLowerCase();
16534
+ const hintText = sources.join('\n').toLowerCase();
16535
+ if ((normalizedInfo === 'html' || normalizedInfo.startsWith('html ')) && hintText.includes('index.html')) {
16536
+ return 'index.html';
16537
+ }
16538
+ return '';
16539
+ }
16540
+
16541
+ function materializeOpenAiChatTaskArtifacts(text, options = {}) {
16542
+ const content = typeof text === 'string' ? text : '';
16543
+ if (!content.trim()) {
16544
+ return { files: [], warnings: [] };
16545
+ }
16546
+ const cwd = typeof options.cwd === 'string' && options.cwd.trim() ? options.cwd.trim() : process.cwd();
16547
+ const hints = [options.target, options.notes, options.prompt]
16548
+ .map((item) => String(item || '').trim())
16549
+ .filter(Boolean)
16550
+ .join('\n');
16551
+ const files = [];
16552
+ const warnings = [];
16553
+ const seen = new Set();
16554
+ const fencePattern = /```([^\n`]*)\n([\s\S]*?)```/g;
16555
+ let match = null;
16556
+ while ((match = fencePattern.exec(content)) !== null) {
16557
+ const info = String(match[1] || '').trim();
16558
+ const body = String(match[2] || '');
16559
+ const prefix = content.slice(Math.max(0, match.index - 240), match.index);
16560
+ const inferredPath = inferTaskMaterializedArtifactPath(info, prefix, hints);
16561
+ if (!inferredPath) {
16562
+ continue;
16563
+ }
16564
+ if (Buffer.byteLength(body, 'utf-8') > TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES) {
16565
+ warnings.push(`artifact too large and skipped: ${inferredPath}`);
16566
+ continue;
16567
+ }
16568
+ const normalized = normalizeTaskMaterializedArtifactPath(inferredPath, cwd);
16569
+ if (normalized.error) {
16570
+ warnings.push(normalized.error);
16571
+ continue;
16572
+ }
16573
+ if (seen.has(normalized.path)) {
16574
+ warnings.push(`duplicate artifact skipped: ${normalized.relativePath}`);
16575
+ continue;
16576
+ }
16577
+ seen.add(normalized.path);
16578
+ try {
16579
+ const fileContent = body.trimStart();
16580
+ const parentValidation = validateTaskMaterializedArtifactParents(normalized.path, normalized.baseDir);
16581
+ if (!parentValidation.ok) {
16582
+ warnings.push(parentValidation.error || `artifact parent is unsafe: ${normalized.relativePath}`);
16583
+ continue;
16584
+ }
16585
+ ensureDir(path.dirname(normalized.path));
16586
+ try {
16587
+ if (fs.lstatSync(normalized.path).isSymbolicLink()) {
16588
+ warnings.push(`artifact target is a symlink: ${normalized.relativePath}`);
16589
+ continue;
16590
+ }
16591
+ } catch (error) {
16592
+ if (!error || error.code !== 'ENOENT') {
16593
+ throw error;
16594
+ }
16595
+ }
16596
+ fs.writeFileSync(normalized.path, fileContent, { encoding: 'utf-8', mode: 0o600 });
16597
+ files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: Buffer.byteLength(fileContent, 'utf-8') });
16598
+ } catch (error) {
16599
+ warnings.push(`failed to write artifact ${normalized.relativePath}: ${error && error.message ? error.message : String(error)}`);
16600
+ }
16601
+ }
16602
+ return { files, warnings };
16603
+ }
16604
+
16605
+ async function runOpenAiChatTaskNode(node, context = {}) {
16606
+ const requestConfig = resolveTaskOpenAiChatConfig();
16607
+ if (requestConfig.error) {
16065
16608
  return {
16066
16609
  success: false,
16067
- error: '未找到 codex CLI,请先安装并确保 PATH 可用',
16068
- summary: 'codex CLI 不可用',
16610
+ error: requestConfig.error,
16611
+ summary: requestConfig.error,
16069
16612
  output: null,
16070
- logs: [{ at: toIsoTime(Date.now()), level: 'error', message: 'codex CLI 不可用' }]
16613
+ logs: [{ at: toIsoTime(Date.now()), level: 'error', message: requestConfig.error }]
16614
+ };
16615
+ }
16616
+ const hasApiKey = typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim();
16617
+ const hasExtraHeaders = requestConfig.extraHeaders
16618
+ && typeof requestConfig.extraHeaders === 'object'
16619
+ && !Array.isArray(requestConfig.extraHeaders)
16620
+ && Object.keys(requestConfig.extraHeaders).length > 0;
16621
+ if (!hasApiKey && !hasExtraHeaders) {
16622
+ const error = `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`;
16623
+ return {
16624
+ success: false,
16625
+ error,
16626
+ summary: error,
16627
+ output: {
16628
+ provider: requestConfig.providerName || '',
16629
+ model: requestConfig.model || '',
16630
+ endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''),
16631
+ status: 0,
16632
+ text: '',
16633
+ response: null,
16634
+ durationMs: 0,
16635
+ materializedFiles: []
16636
+ },
16637
+ logs: [{ at: toIsoTime(Date.now()), level: 'error', message: error }]
16071
16638
  };
16072
16639
  }
16073
- const allowWrite = context.allowWrite === true && node.write === true;
16074
- const cwd = typeof context.cwd === 'string' && context.cwd.trim() ? context.cwd.trim() : process.cwd();
16640
+ const allowWrite = context.allowWrite === true && node.write !== false && context.dryRun !== true;
16075
16641
  const dependencyResults = Array.isArray(context.dependencyResults) ? context.dependencyResults : [];
16076
16642
  const dependencyLines = dependencyResults
16077
16643
  .map((item) => {
@@ -16091,124 +16657,95 @@ async function runCodexExecTaskNode(node, context = {}) {
16091
16657
  promptParts.push('请在保持目标不变的前提下修复上一轮失败并继续完成当前节点。');
16092
16658
  }
16093
16659
  const finalPrompt = promptParts.filter(Boolean).join('\n\n');
16094
- const tempRoot = path.join(TASK_RUN_DETAILS_DIR, 'tmp');
16095
- ensureDir(tempRoot);
16096
- const tempDir = fs.mkdtempSync(path.join(tempRoot, 'codex-'));
16097
- const outputFile = path.join(tempDir, 'last-message.txt');
16098
- const args = [
16099
- '-a', 'never',
16100
- '-s', allowWrite ? 'workspace-write' : 'read-only',
16101
- '-C', cwd,
16102
- 'exec',
16103
- '--json',
16104
- '--skip-git-repo-check',
16105
- '--output-last-message', outputFile,
16106
- finalPrompt
16107
- ];
16108
- const stdoutLines = [];
16109
- const stderrLines = [];
16110
- const parsedEvents = [];
16111
- let sessionId = '';
16112
- let stdoutPartial = '';
16113
- let stderrPartial = '';
16114
- const processCapturedLine = (bucket, line) => {
16115
- const normalizedLine = String(line || '').trim();
16116
- if (!normalizedLine) {
16117
- return;
16118
- }
16119
- if (bucket.length < 120) {
16120
- bucket.push(truncateTaskText(normalizedLine, 1200));
16121
- }
16122
- try {
16123
- const payload = JSON.parse(normalizedLine);
16124
- if (parsedEvents.length < 120) {
16125
- parsedEvents.push(payload);
16126
- }
16127
- if (!sessionId) {
16128
- sessionId = findCodexSessionId(payload);
16129
- }
16130
- } catch (_) { }
16131
- };
16132
- const captureLines = (bucket, text, stream) => {
16133
- const currentPartial = stream === 'stderr' ? stderrPartial : stdoutPartial;
16134
- const merged = `${currentPartial}${String(text || '')}`;
16135
- const pieces = merged.split(/\r?\n/g);
16136
- const nextPartial = pieces.pop() || '';
16137
- if (stream === 'stderr') {
16138
- stderrPartial = nextPartial;
16139
- } else {
16140
- stdoutPartial = nextPartial;
16141
- }
16142
- for (const line of pieces) {
16143
- processCapturedLine(bucket, line);
16144
- }
16145
- };
16146
- const flushCapturedPartial = (bucket, stream) => {
16147
- const partial = stream === 'stderr' ? stderrPartial : stdoutPartial;
16148
- if (stream === 'stderr') {
16149
- stderrPartial = '';
16150
- } else {
16151
- stdoutPartial = '';
16152
- }
16153
- processCapturedLine(bucket, partial);
16660
+ const rawThreadId = String(context.threadId || (context.plan && context.plan.threadId) || '').trim();
16661
+ const threadId = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(rawThreadId) ? rawThreadId : '';
16662
+ const threadStoreDir = typeof TASK_WORKSPACE_CHAT_THREADS_DIR === 'string' ? TASK_WORKSPACE_CHAT_THREADS_DIR : '';
16663
+ const canLoadWorkspaceThread = typeof loadWorkspaceChatThread === 'function' && !!threadStoreDir;
16664
+ const thread = threadId && canLoadWorkspaceThread ? loadWorkspaceChatThread(threadStoreDir, threadId) : { messages: [] };
16665
+ const historyMessages = Array.isArray(thread.messages) ? thread.messages.slice(-12) : [];
16666
+ const workspaceContext = typeof buildWorkspaceChatContext === 'function'
16667
+ ? buildWorkspaceChatContext(context.cwd || process.cwd(), finalPrompt, historyMessages)
16668
+ : '';
16669
+ const systemPrompt = [
16670
+ '你是 codexmate 任务编排中的 OpenAI Chat-compatible 执行节点。',
16671
+ '基于用户给出的目标、前置节点摘要、同一 thread 历史和当前工作区快照,输出可执行、可验证、事实谨慎的结果。',
16672
+ allowWrite
16673
+ ? '当前任务允许写入;如果需要创建或更新文件,优先使用 fenced code block:```codexmate-file action="write" path="相对路径"。代码块内容会写入任务工作目录内对应文本文件。删除文件请单独写一行 CODEXMATE_DELETE_FILE: 相对路径。只允许安全相对路径。'
16674
+ : '当前任务是只读/验证节点,不要声称修改了文件。',
16675
+ '查询/读取文件时,先依据用户要求和工作区快照回答;如果快照不足,说明缺口,不要编造。',
16676
+ '回答使用中文,避免空泛总结。'
16677
+ ].join('\n');
16678
+ const currentUserMessage = [
16679
+ finalPrompt,
16680
+ workspaceContext ? '\n--- codexmate workspace snapshot ---' : '',
16681
+ workspaceContext
16682
+ ].filter(Boolean).join('\n');
16683
+ const startedAt = Date.now();
16684
+ const body = {
16685
+ model: requestConfig.model,
16686
+ messages: [
16687
+ { role: 'system', content: systemPrompt },
16688
+ ...historyMessages.map((message) => ({
16689
+ role: message.role === 'assistant' ? 'assistant' : 'user',
16690
+ content: String(message.content || '')
16691
+ })),
16692
+ { role: 'user', content: currentUserMessage }
16693
+ ],
16694
+ temperature: Number.isFinite(requestConfig.temperature) ? requestConfig.temperature : 0.2,
16695
+ stream: false
16154
16696
  };
16155
- const exit = await new Promise((resolve) => {
16156
- const child = spawn(codexPath, args, {
16157
- stdio: ['ignore', 'pipe', 'pipe'],
16158
- windowsHide: true,
16159
- shell: process.platform === 'win32'
16160
- });
16161
- if (typeof context.registerAbort === 'function') {
16162
- context.registerAbort(() => {
16163
- try {
16164
- child.kill('SIGTERM');
16165
- } catch (_) { }
16166
- });
16167
- }
16168
- child.stdout.on('data', (chunk) => {
16169
- captureLines(stdoutLines, chunk, 'stdout');
16170
- });
16171
- child.stderr.on('data', (chunk) => {
16172
- captureLines(stderrLines, chunk, 'stderr');
16173
- });
16174
- child.on('error', (error) => {
16175
- resolve({ code: 1, signal: '', error: error && error.message ? error.message : String(error || 'spawn failed') });
16176
- });
16177
- child.on('close', (code, signal) => {
16178
- flushCapturedPartial(stdoutLines, 'stdout');
16179
- flushCapturedPartial(stderrLines, 'stderr');
16180
- resolve({ code: typeof code === 'number' ? code : 1, signal: signal || '', error: '' });
16181
- });
16697
+ const result = await postOpenAiChatCompletion(requestConfig, body, {
16698
+ registerAbort: context.registerAbort
16182
16699
  });
16183
- const lastMessage = readCodexLastMessageFile(outputFile);
16184
- try {
16185
- if (fs.rmSync) {
16186
- fs.rmSync(tempDir, { recursive: true, force: true });
16187
- } else {
16188
- fs.rmdirSync(tempDir, { recursive: true });
16189
- }
16190
- } catch (_) { }
16191
- const success = exit.code === 0;
16192
- const errorMessage = success
16193
- ? ''
16194
- : (exit.error || stderrLines[stderrLines.length - 1] || stdoutLines[stdoutLines.length - 1] || `codex exec exited with code ${exit.code}`);
16195
- const summary = truncateTaskText(lastMessage || (success ? 'Codex 执行完成' : errorMessage), 400);
16700
+ const text = result.ok ? extractModelResponseText(result.payload) : '';
16701
+ const success = result.ok && !!text;
16702
+ const errorMessage = success ? '' : (result.error || 'OpenAI Chat response did not contain text');
16703
+ const summary = truncateTaskText(text || errorMessage, 400);
16704
+ const safeEndpoint = redactTaskEndpointUrl(requestConfig.endpointUrl);
16705
+ const materialized = success && allowWrite
16706
+ ? materializeOpenAiChatTaskArtifacts(text, {
16707
+ cwd: context.cwd || process.cwd(),
16708
+ target: context.plan && context.plan.target ? context.plan.target : '',
16709
+ notes: context.plan && context.plan.notes ? context.plan.notes : '',
16710
+ prompt: node.prompt || ''
16711
+ })
16712
+ : { files: [], warnings: [] };
16713
+ const workspaceOperations = success && typeof applyWorkspaceFileOperations === 'function'
16714
+ ? applyWorkspaceFileOperations(text, context.cwd || process.cwd(), { allowWrite })
16715
+ : { files: [], warnings: [] };
16716
+ const threadAppend = threadId && typeof appendWorkspaceChatThread === 'function' && !!threadStoreDir
16717
+ ? appendWorkspaceChatThread(threadStoreDir, threadId, [
16718
+ { role: 'user', content: finalPrompt },
16719
+ { role: 'assistant', content: text || errorMessage }
16720
+ ], { cwd: context.cwd || process.cwd() })
16721
+ : { ok: false, error: 'thread id is empty' };
16196
16722
  return {
16197
16723
  success,
16198
16724
  error: errorMessage,
16199
16725
  summary,
16200
16726
  output: {
16201
- exitCode: exit.code,
16202
- signal: exit.signal || '',
16203
- sessionId,
16204
- lastMessage,
16205
- events: parsedEvents,
16206
- stdoutPreview: stdoutLines,
16207
- stderrPreview: stderrLines
16727
+ provider: requestConfig.providerName,
16728
+ model: requestConfig.model,
16729
+ endpoint: safeEndpoint,
16730
+ status: result.status || 0,
16731
+ text,
16732
+ response: result.payload || null,
16733
+ durationMs: Date.now() - startedAt,
16734
+ messageCount: body.messages.length,
16735
+ threadId,
16736
+ threadStore: threadAppend && threadAppend.file ? threadAppend.file : '',
16737
+ materializedFiles: materialized.files,
16738
+ workspaceFiles: workspaceOperations.files
16208
16739
  },
16209
16740
  logs: [
16210
- ...stdoutLines.map((line) => ({ at: toIsoTime(Date.now()), level: 'info', message: line })),
16211
- ...stderrLines.map((line) => ({ at: toIsoTime(Date.now()), level: 'warn', message: line }))
16741
+ { at: toIsoTime(Date.now()), level: 'info', message: `OpenAI Chat request provider=${requestConfig.providerName} model=${requestConfig.model} status=${result.status || 0}` },
16742
+ ...(threadId ? [{ at: toIsoTime(Date.now()), level: 'info', message: `workspace chat thread=${threadId} messages=${body.messages.length}` }] : []),
16743
+ ...(success ? [{ at: toIsoTime(Date.now()), level: 'info', message: truncateTaskText(text, 1200) }] : [{ at: toIsoTime(Date.now()), level: 'error', message: errorMessage }]),
16744
+ ...materialized.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `materialized artifact ${file.relativePath} (${file.bytes} bytes)` })),
16745
+ ...workspaceOperations.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `workspace ${file.operation} ${file.relativePath} (${file.bytes} bytes)` })),
16746
+ ...materialized.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })),
16747
+ ...workspaceOperations.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })),
16748
+ ...(threadAppend && threadAppend.error ? [{ at: toIsoTime(Date.now()), level: 'warn', message: threadAppend.error }] : [])
16212
16749
  ]
16213
16750
  };
16214
16751
  }
@@ -16246,7 +16783,7 @@ async function executeTaskNodeAdapter(node, context = {}) {
16246
16783
  : []
16247
16784
  };
16248
16785
  }
16249
- return runCodexExecTaskNode(node, context);
16786
+ return runOpenAiChatTaskNode(node, context);
16250
16787
  }
16251
16788
 
16252
16789
  async function runTaskPlanInternal(plan, options = {}) {
@@ -16260,13 +16797,18 @@ async function runTaskPlanInternal(plan, options = {}) {
16260
16797
  }
16261
16798
  const taskId = typeof options.taskId === 'string' && options.taskId.trim() ? options.taskId.trim() : (plan.id || createTaskId());
16262
16799
  const runId = typeof options.runId === 'string' && options.runId.trim() ? options.runId.trim() : createTaskRunId();
16800
+ const threadId = normalizeTaskThreadId(options.threadId || plan.threadId) || createTaskThreadId();
16801
+ plan.threadId = threadId;
16802
+ const cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd();
16263
16803
  const controller = new AbortController();
16264
16804
  const baseDetail = {
16265
16805
  runId,
16266
16806
  taskId,
16807
+ threadId,
16267
16808
  workerPid: process.pid,
16268
16809
  title: plan.title || '',
16269
16810
  target: plan.target || '',
16811
+ cwd,
16270
16812
  engine: normalizeTaskEngine(plan.engine),
16271
16813
  allowWrite: plan.allowWrite === true,
16272
16814
  dryRun: plan.dryRun === true,
@@ -16302,6 +16844,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16302
16844
  const queued = upsertTaskQueueItem({
16303
16845
  ...options.queueItem,
16304
16846
  taskId,
16847
+ threadId,
16848
+ cwd,
16305
16849
  status: 'running',
16306
16850
  runStatus: 'running',
16307
16851
  lastRunId: runId,
@@ -16320,9 +16864,10 @@ async function runTaskPlanInternal(plan, options = {}) {
16320
16864
  plan,
16321
16865
  taskId,
16322
16866
  runId,
16867
+ threadId,
16323
16868
  allowWrite: plan.allowWrite === true,
16324
16869
  dryRun: plan.dryRun === true,
16325
- cwd: plan.cwd || process.cwd()
16870
+ cwd
16326
16871
  }),
16327
16872
  onUpdate: async (snapshot) => {
16328
16873
  const nextDetail = {
@@ -16336,6 +16881,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16336
16881
  const queued = upsertTaskQueueItem({
16337
16882
  ...options.queueItem,
16338
16883
  taskId,
16884
+ threadId,
16885
+ cwd,
16339
16886
  status: snapshot.status === 'success'
16340
16887
  ? 'completed'
16341
16888
  : (snapshot.status === 'failed' ? 'failed' : (snapshot.status === 'cancelled' ? 'cancelled' : 'running')),
@@ -16365,6 +16912,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16365
16912
  const queued = upsertTaskQueueItem({
16366
16913
  ...options.queueItem,
16367
16914
  taskId,
16915
+ threadId,
16916
+ cwd,
16368
16917
  status: run.status === 'success'
16369
16918
  ? 'completed'
16370
16919
  : (run.status === 'cancelled' ? 'cancelled' : 'failed'),
@@ -16395,8 +16944,10 @@ function addTaskToQueue(params = {}) {
16395
16944
  const taskId = typeof params.taskId === 'string' && params.taskId.trim() ? params.taskId.trim() : createTaskId();
16396
16945
  const item = upsertTaskQueueItem({
16397
16946
  taskId,
16947
+ threadId: plan.threadId || createTaskThreadId(),
16398
16948
  title: plan.title,
16399
16949
  target: plan.target,
16950
+ cwd: plan.cwd || process.cwd(),
16400
16951
  status: 'queued',
16401
16952
  createdAt: toIsoTime(Date.now()),
16402
16953
  updatedAt: toIsoTime(Date.now()),