codexmate 0.0.56 → 0.1.1

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 (42) hide show
  1. package/README.md +2 -2
  2. package/README.vi.md +1 -0
  3. package/README.zh.md +2 -2
  4. package/cli/openai-bridge-retry.js +62 -0
  5. package/cli/openai-bridge-runtime.js +1819 -0
  6. package/cli/openai-bridge.js +137 -2048
  7. package/cli.js +749 -133
  8. package/lib/task-orchestrator.js +90 -21
  9. package/lib/task-workspace-chat.js +292 -0
  10. package/package.json +2 -2
  11. package/web-ui/app.js +67 -131
  12. package/web-ui/modules/app.computed.main-tabs.mjs +304 -7
  13. package/web-ui/modules/app.methods.agents.mjs +192 -1
  14. package/web-ui/modules/app.methods.claude-config.mjs +65 -25
  15. package/web-ui/modules/app.methods.navigation.mjs +67 -83
  16. package/web-ui/modules/app.methods.openclaw-editing.mjs +3 -6
  17. package/web-ui/modules/app.methods.providers.mjs +40 -10
  18. package/web-ui/modules/app.methods.session-actions.mjs +1 -12
  19. package/web-ui/modules/app.methods.session-browser.mjs +23 -54
  20. package/web-ui/modules/app.methods.session-trash.mjs +0 -1
  21. package/web-ui/modules/app.methods.startup-claude.mjs +16 -31
  22. package/web-ui/modules/app.methods.task-orchestration.mjs +347 -8
  23. package/web-ui/modules/app.methods.tool-config-permissions.mjs +0 -3
  24. package/web-ui/modules/app.methods.web-ui-preferences.mjs +442 -68
  25. package/web-ui/modules/config-template-confirm-pref.mjs +2 -3
  26. package/web-ui/modules/i18n/locales/en.mjs +181 -30
  27. package/web-ui/modules/i18n/locales/ja.mjs +178 -27
  28. package/web-ui/modules/i18n/locales/vi.mjs +180 -29
  29. package/web-ui/modules/i18n/locales/zh-tw.mjs +181 -30
  30. package/web-ui/modules/i18n/locales/zh.mjs +183 -32
  31. package/web-ui/modules/i18n.mjs +5 -12
  32. package/web-ui/modules/sessions-filters-url.mjs +1 -2
  33. package/web-ui/partials/index/layout-header.html +44 -38
  34. package/web-ui/partials/index/modals-basic.html +26 -0
  35. package/web-ui/partials/index/panel-orchestration.html +489 -282
  36. package/web-ui/partials/index/panel-prompts.html +66 -3
  37. package/web-ui/res/web-ui-render.precompiled.js +1181 -604
  38. package/web-ui/styles/layout-shell.css +157 -1
  39. package/web-ui/styles/modals-core.css +173 -0
  40. package/web-ui/styles/navigation-panels.css +11 -0
  41. package/web-ui/styles/responsive.css +32 -0
  42. 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
  }
@@ -2477,6 +2561,28 @@ function buildClaudeSettingsDiff(params = {}) {
2477
2561
  };
2478
2562
  }
2479
2563
 
2564
+
2565
+ function normalizeOpenaiBridgeMaxRetries(value, fallback = 2) {
2566
+ const raw = Number(value);
2567
+ const fallbackRaw = Number(fallback);
2568
+ const base = Number.isFinite(raw) ? raw : (Number.isFinite(fallbackRaw) ? fallbackRaw : 2);
2569
+ return Math.min(10, Math.max(2, Math.floor(base)));
2570
+ }
2571
+
2572
+ function resolveProviderOpenaiBridgeMaxRetries(provider) {
2573
+ if (!provider || typeof provider !== 'object') return 2;
2574
+ if (provider.codexmate_bridge_max_retries !== undefined) {
2575
+ return normalizeOpenaiBridgeMaxRetries(provider.codexmate_bridge_max_retries);
2576
+ }
2577
+ if (provider.openai_bridge_max_retries !== undefined) {
2578
+ return normalizeOpenaiBridgeMaxRetries(provider.openai_bridge_max_retries);
2579
+ }
2580
+ if (provider.max_retries !== undefined) {
2581
+ return normalizeOpenaiBridgeMaxRetries(provider.max_retries);
2582
+ }
2583
+ return 2;
2584
+ }
2585
+
2480
2586
  function addProviderToConfig(params = {}) {
2481
2587
  const name = typeof params.name === 'string' ? params.name.trim() : '';
2482
2588
  const url = typeof params.url === 'string' ? params.url.trim() : '';
@@ -2491,6 +2597,8 @@ function addProviderToConfig(params = {}) {
2491
2597
  ? params.model.trim()
2492
2598
  : fallbackModel;
2493
2599
  const useTransform = !!params.useTransform;
2600
+ const hasOpenaiBridgeMaxRetries = params.openaiBridgeMaxRetries !== undefined || params.maxRetries !== undefined;
2601
+ const openaiBridgeMaxRetries = normalizeOpenaiBridgeMaxRetries(params.openaiBridgeMaxRetries ?? params.maxRetries);
2494
2602
  const allowManaged = !!params.allowManaged;
2495
2603
  const normalizedUrl = normalizeBaseUrl(url);
2496
2604
 
@@ -2550,7 +2658,7 @@ function addProviderToConfig(params = {}) {
2550
2658
  const requiresOpenaiAuth = useTransform;
2551
2659
 
2552
2660
  if (useTransform) {
2553
- const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, normalizedUrl, key);
2661
+ const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, normalizedUrl, key, undefined, { maxRetries: openaiBridgeMaxRetries });
2554
2662
  if (saveRes && saveRes.error) {
2555
2663
  return { error: String(saveRes.error) };
2556
2664
  }
@@ -2562,6 +2670,7 @@ function addProviderToConfig(params = {}) {
2562
2670
  ).toString().replace(/\/+$/g, '');
2563
2671
  authKeyForConfig = 'codexmate';
2564
2672
  extraLines.push(`codexmate_bridge = "openai"`);
2673
+ extraLines.push(`codexmate_bridge_max_retries = ${openaiBridgeMaxRetries}`);
2565
2674
  }
2566
2675
 
2567
2676
  const safeUrl = escapeTomlBasicString(baseUrlForConfig);
@@ -2605,6 +2714,8 @@ function updateProviderInConfig(params = {}) {
2605
2714
  ? String(params.key).trim()
2606
2715
  : undefined;
2607
2716
  const useTransform = !!params.useTransform;
2717
+ const hasOpenaiBridgeMaxRetries = params.openaiBridgeMaxRetries !== undefined || params.maxRetries !== undefined;
2718
+ const openaiBridgeMaxRetries = normalizeOpenaiBridgeMaxRetries(params.openaiBridgeMaxRetries ?? params.maxRetries);
2608
2719
  const allowManaged = !!params.allowManaged;
2609
2720
 
2610
2721
  if (!name) return { error: '名称不能为空' };
@@ -2619,7 +2730,7 @@ function updateProviderInConfig(params = {}) {
2619
2730
  }
2620
2731
 
2621
2732
  try {
2622
- cmdUpdate(name, url || undefined, key, true, { allowManaged, useTransform });
2733
+ cmdUpdate(name, url || undefined, key, true, { allowManaged, useTransform, ...(hasOpenaiBridgeMaxRetries ? { openaiBridgeMaxRetries } : {}) });
2623
2734
  return { success: true };
2624
2735
  } catch (e) {
2625
2736
  return { error: e.message || '更新失败' };
@@ -9762,6 +9873,8 @@ function cmdDelete(name, silent = false) {
9762
9873
  function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
9763
9874
  const allowManaged = !!(options && options.allowManaged);
9764
9875
  const forceUseTransform = !!(options && options.useTransform);
9876
+ const hasOpenaiBridgeMaxRetries = options && Object.prototype.hasOwnProperty.call(options, 'openaiBridgeMaxRetries');
9877
+ const openaiBridgeMaxRetries = normalizeOpenaiBridgeMaxRetries(options && options.openaiBridgeMaxRetries);
9765
9878
  const normalizedBaseUrl = baseUrl === undefined ? undefined : normalizeBaseUrl(baseUrl);
9766
9879
  if (!name) {
9767
9880
  if (!silent) console.error('错误: 提供商名称必填');
@@ -9921,6 +10034,32 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
9921
10034
  return next;
9922
10035
  };
9923
10036
 
10037
+ const replaceTomlNumberField = (block, fieldName, rawValue) => {
10038
+ const numberValue = String(Math.floor(Number(rawValue)));
10039
+ const escapedFieldName = escapeRegex(fieldName);
10040
+ const multilineRanges = collectTomlMultilineStringRanges(block);
10041
+ const withCommentRegex = new RegExp(`^(\\s*${escapedFieldName}\\s*=\\s*)([-+]?\\d+(?:\\.\\d+)?)(\\s+#.*)?$`, 'mg');
10042
+ let replaced = false;
10043
+ let next = block.replace(withCommentRegex, (full, prefix, _value, suffix = '', offset) => {
10044
+ if (replaced || isIndexInRanges(offset, multilineRanges)) {
10045
+ return full;
10046
+ }
10047
+ replaced = true;
10048
+ return `${prefix}${numberValue}${suffix}`;
10049
+ });
10050
+ if (!replaced) {
10051
+ const keyIndentMatch = block.match(/^(\s*)[A-Za-z0-9_.-]+\s*=/m);
10052
+ const indent = keyIndentMatch ? keyIndentMatch[1] : '';
10053
+ const lineEnding = block.includes('\r\n') ? '\r\n' : '\n';
10054
+ const tailMatch = block.match(/(\s*)$/);
10055
+ const tail = tailMatch ? tailMatch[1] : '';
10056
+ const body = block.slice(0, block.length - tail.length);
10057
+ const separator = body.endsWith('\n') || body.endsWith('\r') ? '' : lineEnding;
10058
+ next = `${body}${separator}${indent}${fieldName} = ${numberValue}${tail}`;
10059
+ }
10060
+ return next;
10061
+ };
10062
+
9924
10063
  const replaceTomlBooleanField = (block, fieldName, rawValue) => {
9925
10064
  const boolValue = rawValue ? 'true' : 'false';
9926
10065
  const escapedFieldName = escapeRegex(fieldName);
@@ -9977,7 +10116,9 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
9977
10116
  : existingApiKey;
9978
10117
 
9979
10118
  if (upstreamBaseUrl) {
9980
- const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, upstreamBaseUrl, upstreamApiKey);
10119
+ const saveRes = upsertOpenaiBridgeProvider(OPENAI_BRIDGE_SETTINGS_FILE, name, upstreamBaseUrl, upstreamApiKey, undefined, {
10120
+ maxRetries: hasOpenaiBridgeMaxRetries ? openaiBridgeMaxRetries : resolveProviderOpenaiBridgeMaxRetries(providerConfig)
10121
+ });
9981
10122
  if (saveRes && saveRes.error) {
9982
10123
  throw new Error(String(saveRes.error));
9983
10124
  }
@@ -9993,6 +10134,9 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
9993
10134
  updatedBlock = replaceTomlBooleanField(updatedBlock, 'requires_openai_auth', true);
9994
10135
  updatedBlock = replaceTomlStringField(updatedBlock, 'preferred_auth_method', 'codexmate');
9995
10136
  updatedBlock = replaceTomlStringField(updatedBlock, 'codexmate_bridge', 'openai');
10137
+ if (hasOpenaiBridgeMaxRetries) {
10138
+ updatedBlock = replaceTomlNumberField(updatedBlock, 'codexmate_bridge_max_retries', openaiBridgeMaxRetries);
10139
+ }
9996
10140
  } else {
9997
10141
  if (normalizedBaseUrl) {
9998
10142
  updatedBlock = replaceTomlStringField(updatedBlock, 'base_url', normalizedBaseUrl);
@@ -12735,7 +12879,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
12735
12879
  break;
12736
12880
  }
12737
12881
  // 不返回 apiKey(敏感信息),仅返回用户填过的上游 URL
12738
- result = { baseUrl: upstream.baseUrl, hasApiKey: !!(upstream.apiKey) };
12882
+ const config = readConfig();
12883
+ const provider = config.model_providers && config.model_providers[name];
12884
+ const providerMaxRetries = resolveProviderOpenaiBridgeMaxRetries(provider);
12885
+ result = { baseUrl: upstream.baseUrl, hasApiKey: !!(upstream.apiKey), maxRetries: providerMaxRetries };
12739
12886
  break;
12740
12887
  }
12741
12888
  case 'list-sessions':
@@ -13015,6 +13162,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13015
13162
  detached: true,
13016
13163
  taskId,
13017
13164
  runId,
13165
+ threadId: plan.threadId || '',
13166
+ cwd: plan.cwd || '',
13018
13167
  warnings: validation.warnings || []
13019
13168
  };
13020
13169
  } else {
@@ -13859,7 +14008,11 @@ function printTaskHelp() {
13859
14008
  console.log(' --allow-write 允许写入工作区');
13860
14009
  console.log(' --dry-run 仅计划/预演,不执行写入');
13861
14010
  console.log(' --plan-only 仅输出计划,不执行');
13862
- console.log(' --engine <codex|workflow> 选择编排引擎');
14011
+ console.log(' --engine <openai-chat|workflow> 选择编排引擎');
14012
+ console.log(' --cwd <路径> 指定任务工作区路径');
14013
+ console.log(' --thread-id <ID> 指定任务线程 ID');
14014
+ console.log(' --conversation-id <ID> 指定任务线程 ID(兼容别名)');
14015
+ console.log(' --session-id <ID> 指定任务线程 ID(兼容别名)');
13863
14016
  console.log(' --concurrency <N> 并发度');
13864
14017
  console.log(' --auto-fix-rounds <N> 自动修复回合数');
13865
14018
  console.log(' --limit <N> runs/queue list 数量');
@@ -13882,7 +14035,7 @@ function parseTaskCliOptions(args = []) {
13882
14035
  allowWrite: false,
13883
14036
  dryRun: false,
13884
14037
  planOnly: false,
13885
- engine: 'codex',
14038
+ engine: 'openai-chat',
13886
14039
  concurrency: 2,
13887
14040
  autoFixRounds: 1,
13888
14041
  limit: 20,
@@ -14029,6 +14182,38 @@ function parseTaskCliOptions(args = []) {
14029
14182
  options.explicit.autoFixRounds = true;
14030
14183
  continue;
14031
14184
  }
14185
+ if (arg === '--cwd') {
14186
+ options.cwd = String(args[i + 1] || '').trim();
14187
+ options.explicit.cwd = true;
14188
+ i += 1;
14189
+ continue;
14190
+ }
14191
+ if (arg.startsWith('--cwd=')) {
14192
+ options.cwd = arg.slice('--cwd='.length).trim();
14193
+ options.explicit.cwd = true;
14194
+ continue;
14195
+ }
14196
+ if (arg === '--thread-id' || arg === '--conversation-id' || arg === '--session-id') {
14197
+ options.threadId = String(args[i + 1] || '').trim();
14198
+ options.explicit.threadId = true;
14199
+ i += 1;
14200
+ continue;
14201
+ }
14202
+ if (arg.startsWith('--thread-id=')) {
14203
+ options.threadId = arg.slice('--thread-id='.length).trim();
14204
+ options.explicit.threadId = true;
14205
+ continue;
14206
+ }
14207
+ if (arg.startsWith('--conversation-id=')) {
14208
+ options.threadId = arg.slice('--conversation-id='.length).trim();
14209
+ options.explicit.threadId = true;
14210
+ continue;
14211
+ }
14212
+ if (arg.startsWith('--session-id=')) {
14213
+ options.threadId = arg.slice('--session-id='.length).trim();
14214
+ options.explicit.threadId = true;
14215
+ continue;
14216
+ }
14032
14217
  if (arg === '--limit') {
14033
14218
  const value = parseInt(args[i + 1], 10);
14034
14219
  if (Number.isFinite(value)) options.limit = value;
@@ -14086,9 +14271,11 @@ function buildTaskCliPayload(options = {}, rest = []) {
14086
14271
  if (explicit.followUps && Array.isArray(options.followUps)) payload.followUps = options.followUps.slice();
14087
14272
  if (explicit.allowWrite) payload.allowWrite = options.allowWrite === true;
14088
14273
  if (explicit.dryRun) payload.dryRun = options.dryRun === true;
14089
- if (explicit.engine) payload.engine = options.engine || 'codex';
14274
+ if (explicit.engine) payload.engine = options.engine || 'openai-chat';
14090
14275
  if (explicit.concurrency) payload.concurrency = options.concurrency;
14091
14276
  if (explicit.autoFixRounds) payload.autoFixRounds = options.autoFixRounds;
14277
+ if (explicit.cwd && options.cwd) payload.cwd = options.cwd;
14278
+ if (explicit.threadId && options.threadId) payload.threadId = options.threadId;
14092
14279
  if (explicit.taskId && options.taskId) payload.taskId = options.taskId;
14093
14280
  if (explicit.runId && options.runId) payload.runId = options.runId;
14094
14281
  if (!payload.target && Array.isArray(rest) && rest.length > 0) {
@@ -14102,7 +14289,7 @@ function buildTaskCliPayload(options = {}, rest = []) {
14102
14289
 
14103
14290
  function printTaskPlanSummary(plan, warnings = []) {
14104
14291
  console.log(`\n任务计划: ${plan.title || '(untitled)'}`);
14105
- console.log(` engine: ${plan.engine || 'codex'}`);
14292
+ console.log(` engine: ${plan.engine || 'openai-chat'}`);
14106
14293
  console.log(` allowWrite: ${plan.allowWrite === true ? 'yes' : 'no'}`);
14107
14294
  console.log(` dryRun: ${plan.dryRun === true ? 'yes' : 'no'}`);
14108
14295
  console.log(` concurrency: ${plan.concurrency || 1}`);
@@ -14935,11 +15122,13 @@ function buildMcpProviderListPayload() {
14935
15122
  upstreamUrl = upstream.baseUrl.trim();
14936
15123
  }
14937
15124
  }
15125
+ const openaiBridgeMaxRetries = resolveProviderOpenaiBridgeMaxRetries(p);
14938
15126
  return {
14939
15127
  name,
14940
15128
  url: p.base_url || '',
14941
15129
  upstreamUrl,
14942
15130
  codexmate_bridge: bridge,
15131
+ openaiBridgeMaxRetries,
14943
15132
  key: maskKey(p.preferred_auth_method || ''),
14944
15133
  hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
14945
15134
  models: Array.isArray(p.models)
@@ -15556,6 +15745,10 @@ function createTaskRunId() {
15556
15745
  return `tr-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
15557
15746
  }
15558
15747
 
15748
+ function createTaskThreadId() {
15749
+ return `tt-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
15750
+ }
15751
+
15559
15752
  function validateTaskRunId(value) {
15560
15753
  const runId = typeof value === 'string' ? value.trim() : '';
15561
15754
  if (!runId) {
@@ -15567,9 +15760,19 @@ function validateTaskRunId(value) {
15567
15760
  return { ok: true, error: '', runId };
15568
15761
  }
15569
15762
 
15763
+ function normalizeTaskThreadId(value) {
15764
+ const threadId = typeof value === 'string' ? value.trim() : '';
15765
+ if (!threadId) {
15766
+ return '';
15767
+ }
15768
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(threadId) ? threadId : '';
15769
+ }
15770
+
15570
15771
  function normalizeTaskEngine(value) {
15571
- const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
15572
- return normalized === 'workflow' ? 'workflow' : 'codex';
15772
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s_/]+/g, '-') : '';
15773
+ if (normalized === 'workflow') return 'workflow';
15774
+ // Legacy task plans used `codex`; new task execution uses the configured OpenAI Chat-compatible provider.
15775
+ return 'openai-chat';
15573
15776
  }
15574
15777
 
15575
15778
  function normalizeTaskFollowUps(input = []) {
@@ -15610,6 +15813,7 @@ function normalizeTaskPlanRequest(params = {}) {
15610
15813
  : (typeof source.followUp === 'string' && source.followUp.trim() ? [source.followUp.trim()] : []);
15611
15814
  return {
15612
15815
  id: typeof source.id === 'string' ? source.id.trim() : '',
15816
+ threadId: normalizeTaskThreadId(source.threadId || source.conversationId || source.sessionId),
15613
15817
  title: typeof source.title === 'string' ? source.title.trim() : '',
15614
15818
  target: typeof source.target === 'string' ? source.target.trim() : '',
15615
15819
  notes: typeof source.notes === 'string' ? source.notes.trim() : '',
@@ -15619,6 +15823,7 @@ function normalizeTaskPlanRequest(params = {}) {
15619
15823
  dryRun: source.dryRun === true,
15620
15824
  concurrency: Number.isFinite(source.concurrency) ? source.concurrency : parseInt(source.concurrency, 10),
15621
15825
  autoFixRounds: Number.isFinite(source.autoFixRounds) ? source.autoFixRounds : parseInt(source.autoFixRounds, 10),
15826
+ previewOnly: source.previewOnly === true,
15622
15827
  workflowIds: rawWorkflowIds,
15623
15828
  followUps: normalizeTaskFollowUps(rawFollowUps)
15624
15829
  };
@@ -15627,12 +15832,18 @@ function normalizeTaskPlanRequest(params = {}) {
15627
15832
  function coerceTaskPlanPayload(params = {}) {
15628
15833
  if (params && params.plan && typeof params.plan === 'object' && !Array.isArray(params.plan)) {
15629
15834
  const plan = cloneJson(params.plan, {});
15630
- const overrideKeys = ['id', 'title', 'target', 'notes', 'cwd', 'engine', 'allowWrite', 'dryRun', 'concurrency', 'autoFixRounds', 'workflowIds', 'followUps'];
15835
+ const overrideKeys = ['id', 'threadId', 'conversationId', 'sessionId', 'title', 'target', 'notes', 'cwd', 'engine', 'allowWrite', 'dryRun', 'concurrency', 'autoFixRounds', 'previewOnly', 'workflowIds', 'followUps'];
15631
15836
  for (const key of overrideKeys) {
15632
15837
  if (Object.prototype.hasOwnProperty.call(params, key) && params[key] !== undefined) {
15633
- plan[key] = cloneJson(params[key], params[key]);
15838
+ if (key === 'conversationId' || key === 'sessionId') {
15839
+ plan.threadId = cloneJson(params[key], params[key]);
15840
+ } else {
15841
+ plan[key] = cloneJson(params[key], params[key]);
15842
+ }
15634
15843
  }
15635
15844
  }
15845
+ plan.cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd();
15846
+ plan.threadId = normalizeTaskThreadId(plan.threadId) || createTaskThreadId();
15636
15847
  plan.engine = normalizeTaskEngine(plan.engine);
15637
15848
  plan.workflowIds = normalizeTaskFollowUps(plan.workflowIds || []).map((id) => normalizeWorkflowId(id)).filter(Boolean);
15638
15849
  plan.followUps = normalizeTaskFollowUps(plan.followUps || []);
@@ -15647,6 +15858,7 @@ function coerceTaskPlanPayload(params = {}) {
15647
15858
  });
15648
15859
  return {
15649
15860
  ...plan,
15861
+ threadId: request.threadId || createTaskThreadId(),
15650
15862
  engine: normalizeTaskEngine(request.engine || plan.engine)
15651
15863
  };
15652
15864
  }
@@ -15669,8 +15881,10 @@ function normalizeTaskQueueItem(raw = {}) {
15669
15881
  const taskId = typeof raw.taskId === 'string' ? raw.taskId.trim() : '';
15670
15882
  return {
15671
15883
  taskId: taskId || createTaskId(),
15884
+ threadId: normalizeTaskThreadId(raw.threadId || plan.threadId) || createTaskThreadId(),
15672
15885
  title: typeof raw.title === 'string' ? raw.title.trim() : (typeof plan.title === 'string' ? plan.title.trim() : ''),
15673
15886
  target: typeof raw.target === 'string' ? raw.target.trim() : (typeof plan.target === 'string' ? plan.target.trim() : ''),
15887
+ cwd: typeof raw.cwd === 'string' && raw.cwd.trim() ? raw.cwd.trim() : (typeof plan.cwd === 'string' ? plan.cwd.trim() : ''),
15674
15888
  status: typeof raw.status === 'string' ? raw.status.trim().toLowerCase() : 'queued',
15675
15889
  createdAt: toIsoTime(raw.createdAt || Date.now(), ''),
15676
15890
  updatedAt: toIsoTime(raw.updatedAt || raw.createdAt || Date.now(), ''),
@@ -15871,8 +16085,10 @@ function collectTaskRunSummary(detail = {}) {
15871
16085
  return {
15872
16086
  runId: detail.runId || '',
15873
16087
  taskId: detail.taskId || '',
16088
+ threadId: detail.threadId || '',
15874
16089
  title: detail.title || '',
15875
16090
  target: detail.target || '',
16091
+ cwd: detail.cwd || '',
15876
16092
  engine: detail.engine || '',
15877
16093
  allowWrite: detail.allowWrite === true,
15878
16094
  dryRun: detail.dryRun === true,
@@ -15996,6 +16212,7 @@ function buildTaskOverviewPayload(options = {}) {
15996
16212
  }
15997
16213
  return {
15998
16214
  workflows: workflowCatalog.workflows,
16215
+ openAiChatStatus: buildTaskOpenAiChatStatus(),
15999
16216
  warnings,
16000
16217
  queue,
16001
16218
  runs,
@@ -16058,20 +16275,434 @@ function readCodexLastMessageFile(filePath) {
16058
16275
  }
16059
16276
  }
16060
16277
 
16061
- async function runCodexExecTaskNode(node, context = {}) {
16062
- const codexPath = resolveSpawnCommand('codex');
16063
- const codexProbeCommand = process.platform === 'win32' ? 'codex' : codexPath;
16064
- if (!commandExists(codexProbeCommand, '--version')) {
16278
+ function buildOpenAiChatEndpointUrl(baseUrl) {
16279
+ const trimmed = normalizeBaseUrl(baseUrl);
16280
+ if (!trimmed) return '';
16281
+ if (/\/v1\/chat\/completions$/i.test(trimmed) || /\/chat\/completions$/i.test(trimmed)) {
16282
+ return trimmed;
16283
+ }
16284
+ return joinApiUrl(trimmed, 'chat/completions');
16285
+ }
16286
+
16287
+ function redactTaskEndpointUrl(endpointUrl = '') {
16288
+ const raw = String(endpointUrl || '').trim();
16289
+ if (!raw) return '';
16290
+ try {
16291
+ const parsed = new URL(raw);
16292
+ if (parsed.username) parsed.username = '***';
16293
+ if (parsed.password) parsed.password = '***';
16294
+ const secretKeyPattern = /(?:^|[_-])(?:key|api[-_]?key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|credential|credentials|signature|sig)(?:$|[_-])/i;
16295
+ for (const key of [...parsed.searchParams.keys()]) {
16296
+ if (secretKeyPattern.test(key)) {
16297
+ parsed.searchParams.set(key, '***');
16298
+ }
16299
+ }
16300
+ return parsed.toString();
16301
+ } catch (_) {
16302
+ return raw
16303
+ .replace(/\/\/([^/@:]+):([^/@]+)@/g, '//***:***@')
16304
+ .replace(/([?&][^=&]*(?:key|token|secret|password|passwd|pwd|credential|signature|sig)[^=]*=)[^&]*/ig, '$1***');
16305
+ }
16306
+ }
16307
+
16308
+ function pickTaskProviderModel(providerName, provider, config) {
16309
+ const currentModels = readCurrentModels();
16310
+ const savedModel = currentModels && typeof currentModels[providerName] === 'string'
16311
+ ? currentModels[providerName].trim()
16312
+ : '';
16313
+ const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
16314
+ const activeModel = typeof config.model === 'string' ? config.model.trim() : '';
16315
+ const providerModels = Array.isArray(provider && provider.models) ? provider.models : [];
16316
+ const firstProviderModel = providerModels
16317
+ .map((item) => {
16318
+ if (typeof item === 'string') return item.trim();
16319
+ if (item && typeof item === 'object') return String(item.id || item.name || item.model || '').trim();
16320
+ return '';
16321
+ })
16322
+ .find(Boolean) || '';
16323
+ return savedModel || (activeProvider === providerName ? activeModel : '') || firstProviderModel;
16324
+ }
16325
+
16326
+ function pickTaskProviderTemperature(provider) {
16327
+ const raw = provider && Object.prototype.hasOwnProperty.call(provider, 'temperature')
16328
+ ? provider.temperature
16329
+ : undefined;
16330
+ if (raw === undefined || raw === null || String(raw).trim() === '') {
16331
+ return 0.2;
16332
+ }
16333
+ const value = Number(raw);
16334
+ if (!Number.isFinite(value) || value < 0 || value > 2) {
16335
+ return 0.2;
16336
+ }
16337
+ return value;
16338
+ }
16339
+
16340
+ function pickTaskOpenAiChatProviderName(config) {
16341
+ const taskProvider = typeof config.task_openai_chat_provider === 'string'
16342
+ ? config.task_openai_chat_provider.trim()
16343
+ : '';
16344
+ if (taskProvider) {
16345
+ return taskProvider;
16346
+ }
16347
+ return typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
16348
+ }
16349
+
16350
+ function resolveTaskOpenAiChatConfig() {
16351
+ const configResult = readConfigOrVirtualDefault();
16352
+ const config = configResult && configResult.config && typeof configResult.config === 'object' ? configResult.config : {};
16353
+ const providerName = pickTaskOpenAiChatProviderName(config);
16354
+ if (!providerName) {
16355
+ return { error: '未设置当前 OpenAI Chat 提供商' };
16356
+ }
16357
+ const providers = config.model_providers && typeof config.model_providers === 'object' ? config.model_providers : {};
16358
+ const provider = providers[providerName];
16359
+ if (!provider || typeof provider !== 'object') {
16360
+ return { error: `OpenAI Chat 提供商不存在: ${providerName}` };
16361
+ }
16362
+
16363
+ const bridgeType = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
16364
+ const isOpenaiBridgeProvider = bridgeType === 'openai'
16365
+ || (typeof provider.base_url === 'string' && provider.base_url.includes('/bridge/openai/'));
16366
+ let baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
16367
+ let apiKey = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
16368
+ let extraHeaders = {};
16369
+ if (isOpenaiBridgeProvider) {
16370
+ const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, providerName);
16371
+ if (upstream && !upstream.error) {
16372
+ baseUrl = upstream.baseUrl || baseUrl;
16373
+ apiKey = upstream.apiKey || apiKey;
16374
+ extraHeaders = upstream.headers && typeof upstream.headers === 'object' && !Array.isArray(upstream.headers)
16375
+ ? upstream.headers
16376
+ : {};
16377
+ }
16378
+ }
16379
+
16380
+ const model = pickTaskProviderModel(providerName, provider, config);
16381
+ if (!baseUrl) {
16382
+ return { error: `OpenAI Chat 提供商 ${providerName} 缺少 base_url` };
16383
+ }
16384
+ if (!isValidHttpUrl(baseUrl)) {
16385
+ return { error: `OpenAI Chat 提供商 ${providerName} 的 base_url 无效` };
16386
+ }
16387
+ if (!model) {
16388
+ return { error: `OpenAI Chat 提供商 ${providerName} 未设置模型` };
16389
+ }
16390
+ return {
16391
+ providerName,
16392
+ baseUrl,
16393
+ endpointUrl: buildOpenAiChatEndpointUrl(baseUrl),
16394
+ apiKey,
16395
+ extraHeaders,
16396
+ model,
16397
+ temperature: pickTaskProviderTemperature(provider)
16398
+ };
16399
+ }
16400
+
16401
+ function buildTaskOpenAiChatStatus() {
16402
+ const requestConfig = resolveTaskOpenAiChatConfig();
16403
+ if (requestConfig && requestConfig.error) {
16404
+ return {
16405
+ ok: false,
16406
+ ready: false,
16407
+ error: requestConfig.error,
16408
+ providerName: '',
16409
+ model: '',
16410
+ endpoint: '',
16411
+ hasApiKey: false,
16412
+ hasExtraHeaders: false
16413
+ };
16414
+ }
16415
+ const hasApiKey = !!(requestConfig && typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim());
16416
+ const hasExtraHeaders = !!(requestConfig
16417
+ && requestConfig.extraHeaders
16418
+ && typeof requestConfig.extraHeaders === 'object'
16419
+ && !Array.isArray(requestConfig.extraHeaders)
16420
+ && Object.keys(requestConfig.extraHeaders).length > 0);
16421
+ const hasAuthMaterial = hasApiKey || hasExtraHeaders;
16422
+ return {
16423
+ ok: true,
16424
+ ready: hasAuthMaterial,
16425
+ error: hasAuthMaterial ? '' : `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`,
16426
+ providerName: requestConfig.providerName || '',
16427
+ model: requestConfig.model || '',
16428
+ endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''),
16429
+ hasApiKey,
16430
+ hasExtraHeaders
16431
+ };
16432
+ }
16433
+
16434
+ function postOpenAiChatCompletion(requestConfig, body, options = {}) {
16435
+ const endpointUrl = requestConfig && requestConfig.endpointUrl ? requestConfig.endpointUrl : '';
16436
+ return new Promise((resolve) => {
16437
+ let parsed;
16438
+ try {
16439
+ parsed = new URL(endpointUrl);
16440
+ } catch (error) {
16441
+ resolve({ ok: false, error: 'OpenAI Chat endpoint URL 无效', status: 0, payload: null, body: '' });
16442
+ return;
16443
+ }
16444
+ const transport = parsed.protocol === 'https:' ? https : http;
16445
+ const agent = parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT;
16446
+ const payloadText = JSON.stringify(body || {});
16447
+ const headers = {
16448
+ 'Content-Type': 'application/json',
16449
+ 'Accept': 'application/json',
16450
+ 'User-Agent': 'codexmate-task-orchestration',
16451
+ 'Content-Length': Buffer.byteLength(payloadText, 'utf-8'),
16452
+ ...(requestConfig.extraHeaders && typeof requestConfig.extraHeaders === 'object' ? requestConfig.extraHeaders : {})
16453
+ };
16454
+ if (requestConfig.apiKey) {
16455
+ headers.Authorization = `Bearer ${requestConfig.apiKey}`;
16456
+ }
16457
+ let settled = false;
16458
+ let req = null;
16459
+ const finish = (result) => {
16460
+ if (settled) return;
16461
+ settled = true;
16462
+ resolve(result);
16463
+ };
16464
+ req = transport.request(parsed, { method: 'POST', headers, agent }, (res) => {
16465
+ const status = res.statusCode || 0;
16466
+ let raw = '';
16467
+ let receivedBytes = 0;
16468
+ res.on('data', (chunk) => {
16469
+ if (settled) return;
16470
+ receivedBytes += chunk.length || 0;
16471
+ if (receivedBytes > TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES) {
16472
+ finish({ ok: false, status, error: 'OpenAI Chat response too large', payload: null, body: raw });
16473
+ res.destroy();
16474
+ return;
16475
+ }
16476
+ raw += chunk;
16477
+ });
16478
+ res.on('error', (error) => {
16479
+ finish({
16480
+ ok: false,
16481
+ status,
16482
+ error: error && error.message ? error.message : 'OpenAI Chat request failed',
16483
+ payload: null,
16484
+ body: raw
16485
+ });
16486
+ });
16487
+ res.on('end', () => {
16488
+ if (settled) return;
16489
+ let parsedPayload = null;
16490
+ try {
16491
+ parsedPayload = raw ? JSON.parse(raw) : null;
16492
+ } catch (_) {}
16493
+ if (status < 200 || status >= 300) {
16494
+ const message = parsedPayload && parsedPayload.error
16495
+ ? (typeof parsedPayload.error === 'string' ? parsedPayload.error : (parsedPayload.error.message || JSON.stringify(parsedPayload.error)))
16496
+ : (raw || `OpenAI Chat request failed: ${status}`);
16497
+ finish({ ok: false, status, error: truncateTaskText(message, 1200), payload: parsedPayload, body: raw });
16498
+ return;
16499
+ }
16500
+ finish({ ok: true, status, error: '', payload: parsedPayload, body: raw });
16501
+ });
16502
+ });
16503
+ if (typeof options.registerAbort === 'function') {
16504
+ options.registerAbort(() => {
16505
+ try {
16506
+ req.destroy(new Error('cancelled'));
16507
+ } catch (_) {}
16508
+ });
16509
+ }
16510
+ req.setTimeout(TASK_OPENAI_CHAT_TIMEOUT_MS, () => {
16511
+ req.destroy(new Error('OpenAI Chat request timeout'));
16512
+ });
16513
+ req.on('error', (error) => {
16514
+ finish({ ok: false, status: 0, error: error && error.message ? error.message : String(error || 'OpenAI Chat request failed'), payload: null, body: '' });
16515
+ });
16516
+ req.write(payloadText);
16517
+ req.end();
16518
+ });
16519
+ }
16520
+
16521
+ const TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES = 512 * 1024;
16522
+ const TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.md', '.txt', '.svg']);
16523
+
16524
+ function normalizeTaskMaterializedArtifactPath(rawPath, cwd) {
16525
+ const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd());
16526
+ const text = typeof rawPath === 'string' ? rawPath.trim() : '';
16527
+ if (!text || text.length > 200) {
16528
+ return { error: 'artifact path is empty or too long' };
16529
+ }
16530
+ if (path.isAbsolute(text) || text.includes('\\')) {
16531
+ return { error: `artifact path must be a safe relative path: ${text}` };
16532
+ }
16533
+ const normalized = path.normalize(text);
16534
+ if (!normalized || normalized === '.' || normalized.startsWith('..') || normalized.split(path.sep).includes('..')) {
16535
+ return { error: `artifact path escapes cwd: ${text}` };
16536
+ }
16537
+ const ext = path.extname(normalized).toLowerCase();
16538
+ if (!TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS.has(ext)) {
16539
+ return { error: `artifact extension is not allowed: ${ext || '(none)'}` };
16540
+ }
16541
+ const targetPath = path.resolve(baseDir, normalized);
16542
+ const relative = path.relative(baseDir, targetPath);
16543
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
16544
+ return { error: `artifact path escapes cwd: ${text}` };
16545
+ }
16546
+ return { path: targetPath, relativePath: normalized, baseDir };
16547
+ }
16548
+
16549
+ function validateTaskMaterializedArtifactParents(targetPath, baseDir) {
16550
+ const resolvedBase = fs.realpathSync.native(baseDir);
16551
+ let current = path.dirname(targetPath);
16552
+ const pending = [];
16553
+ while (true) {
16554
+ if (current === resolvedBase) {
16555
+ return { ok: true };
16556
+ }
16557
+ if (current === path.dirname(current)) {
16558
+ return { ok: false, error: 'artifact parent escapes cwd' };
16559
+ }
16560
+ if (fs.existsSync(current)) {
16561
+ const stats = fs.lstatSync(current);
16562
+ if (stats.isSymbolicLink()) {
16563
+ return { ok: false, error: `artifact parent is a symlink: ${path.relative(baseDir, current) || current}` };
16564
+ }
16565
+ const resolvedCurrent = fs.realpathSync.native(current);
16566
+ const relative = path.relative(resolvedBase, resolvedCurrent);
16567
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
16568
+ return { ok: false, error: 'artifact parent resolves outside cwd' };
16569
+ }
16570
+ for (const child of pending) {
16571
+ const childRelative = path.relative(resolvedBase, child);
16572
+ if (childRelative.startsWith('..') || path.isAbsolute(childRelative)) {
16573
+ return { ok: false, error: 'artifact parent escapes cwd' };
16574
+ }
16575
+ }
16576
+ return { ok: true };
16577
+ }
16578
+ pending.push(current);
16579
+ current = path.dirname(current);
16580
+ }
16581
+ }
16582
+
16583
+ function inferTaskMaterializedArtifactPath(info, prefix, hints) {
16584
+ const sources = [info, prefix, hints].map((item) => String(item || '')).filter(Boolean);
16585
+ const explicitPatterns = [
16586
+ /(?:file|filename|path)\s*[:=]\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i,
16587
+ /(?:文件|路径|保存为|写入到?|输出到?)\s*[::]?\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i,
16588
+ /[`"']([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']/i
16589
+ ];
16590
+ for (const source of sources) {
16591
+ for (const pattern of explicitPatterns) {
16592
+ const match = source.match(pattern);
16593
+ if (match && match[1]) {
16594
+ return match[1];
16595
+ }
16596
+ }
16597
+ }
16598
+ const normalizedInfo = String(info || '').trim().toLowerCase();
16599
+ const hintText = sources.join('\n').toLowerCase();
16600
+ if ((normalizedInfo === 'html' || normalizedInfo.startsWith('html ')) && hintText.includes('index.html')) {
16601
+ return 'index.html';
16602
+ }
16603
+ return '';
16604
+ }
16605
+
16606
+ function materializeOpenAiChatTaskArtifacts(text, options = {}) {
16607
+ const content = typeof text === 'string' ? text : '';
16608
+ if (!content.trim()) {
16609
+ return { files: [], warnings: [] };
16610
+ }
16611
+ const cwd = typeof options.cwd === 'string' && options.cwd.trim() ? options.cwd.trim() : process.cwd();
16612
+ const hints = [options.target, options.notes, options.prompt]
16613
+ .map((item) => String(item || '').trim())
16614
+ .filter(Boolean)
16615
+ .join('\n');
16616
+ const files = [];
16617
+ const warnings = [];
16618
+ const seen = new Set();
16619
+ const fencePattern = /```([^\n`]*)\n([\s\S]*?)```/g;
16620
+ let match = null;
16621
+ while ((match = fencePattern.exec(content)) !== null) {
16622
+ const info = String(match[1] || '').trim();
16623
+ const body = String(match[2] || '');
16624
+ const prefix = content.slice(Math.max(0, match.index - 240), match.index);
16625
+ const inferredPath = inferTaskMaterializedArtifactPath(info, prefix, hints);
16626
+ if (!inferredPath) {
16627
+ continue;
16628
+ }
16629
+ if (Buffer.byteLength(body, 'utf-8') > TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES) {
16630
+ warnings.push(`artifact too large and skipped: ${inferredPath}`);
16631
+ continue;
16632
+ }
16633
+ const normalized = normalizeTaskMaterializedArtifactPath(inferredPath, cwd);
16634
+ if (normalized.error) {
16635
+ warnings.push(normalized.error);
16636
+ continue;
16637
+ }
16638
+ if (seen.has(normalized.path)) {
16639
+ warnings.push(`duplicate artifact skipped: ${normalized.relativePath}`);
16640
+ continue;
16641
+ }
16642
+ seen.add(normalized.path);
16643
+ try {
16644
+ const fileContent = body.trimStart();
16645
+ const parentValidation = validateTaskMaterializedArtifactParents(normalized.path, normalized.baseDir);
16646
+ if (!parentValidation.ok) {
16647
+ warnings.push(parentValidation.error || `artifact parent is unsafe: ${normalized.relativePath}`);
16648
+ continue;
16649
+ }
16650
+ ensureDir(path.dirname(normalized.path));
16651
+ try {
16652
+ if (fs.lstatSync(normalized.path).isSymbolicLink()) {
16653
+ warnings.push(`artifact target is a symlink: ${normalized.relativePath}`);
16654
+ continue;
16655
+ }
16656
+ } catch (error) {
16657
+ if (!error || error.code !== 'ENOENT') {
16658
+ throw error;
16659
+ }
16660
+ }
16661
+ fs.writeFileSync(normalized.path, fileContent, { encoding: 'utf-8', mode: 0o600 });
16662
+ files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: Buffer.byteLength(fileContent, 'utf-8') });
16663
+ } catch (error) {
16664
+ warnings.push(`failed to write artifact ${normalized.relativePath}: ${error && error.message ? error.message : String(error)}`);
16665
+ }
16666
+ }
16667
+ return { files, warnings };
16668
+ }
16669
+
16670
+ async function runOpenAiChatTaskNode(node, context = {}) {
16671
+ const requestConfig = resolveTaskOpenAiChatConfig();
16672
+ if (requestConfig.error) {
16065
16673
  return {
16066
16674
  success: false,
16067
- error: '未找到 codex CLI,请先安装并确保 PATH 可用',
16068
- summary: 'codex CLI 不可用',
16675
+ error: requestConfig.error,
16676
+ summary: requestConfig.error,
16069
16677
  output: null,
16070
- logs: [{ at: toIsoTime(Date.now()), level: 'error', message: 'codex CLI 不可用' }]
16678
+ logs: [{ at: toIsoTime(Date.now()), level: 'error', message: requestConfig.error }]
16071
16679
  };
16072
16680
  }
16073
- const allowWrite = context.allowWrite === true && node.write === true;
16074
- const cwd = typeof context.cwd === 'string' && context.cwd.trim() ? context.cwd.trim() : process.cwd();
16681
+ const hasApiKey = typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim();
16682
+ const hasExtraHeaders = requestConfig.extraHeaders
16683
+ && typeof requestConfig.extraHeaders === 'object'
16684
+ && !Array.isArray(requestConfig.extraHeaders)
16685
+ && Object.keys(requestConfig.extraHeaders).length > 0;
16686
+ if (!hasApiKey && !hasExtraHeaders) {
16687
+ const error = `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`;
16688
+ return {
16689
+ success: false,
16690
+ error,
16691
+ summary: error,
16692
+ output: {
16693
+ provider: requestConfig.providerName || '',
16694
+ model: requestConfig.model || '',
16695
+ endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''),
16696
+ status: 0,
16697
+ text: '',
16698
+ response: null,
16699
+ durationMs: 0,
16700
+ materializedFiles: []
16701
+ },
16702
+ logs: [{ at: toIsoTime(Date.now()), level: 'error', message: error }]
16703
+ };
16704
+ }
16705
+ const allowWrite = context.allowWrite === true && node.write !== false && context.dryRun !== true;
16075
16706
  const dependencyResults = Array.isArray(context.dependencyResults) ? context.dependencyResults : [];
16076
16707
  const dependencyLines = dependencyResults
16077
16708
  .map((item) => {
@@ -16091,124 +16722,95 @@ async function runCodexExecTaskNode(node, context = {}) {
16091
16722
  promptParts.push('请在保持目标不变的前提下修复上一轮失败并继续完成当前节点。');
16092
16723
  }
16093
16724
  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);
16725
+ const rawThreadId = String(context.threadId || (context.plan && context.plan.threadId) || '').trim();
16726
+ const threadId = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(rawThreadId) ? rawThreadId : '';
16727
+ const threadStoreDir = typeof TASK_WORKSPACE_CHAT_THREADS_DIR === 'string' ? TASK_WORKSPACE_CHAT_THREADS_DIR : '';
16728
+ const canLoadWorkspaceThread = typeof loadWorkspaceChatThread === 'function' && !!threadStoreDir;
16729
+ const thread = threadId && canLoadWorkspaceThread ? loadWorkspaceChatThread(threadStoreDir, threadId) : { messages: [] };
16730
+ const historyMessages = Array.isArray(thread.messages) ? thread.messages.slice(-12) : [];
16731
+ const workspaceContext = typeof buildWorkspaceChatContext === 'function'
16732
+ ? buildWorkspaceChatContext(context.cwd || process.cwd(), finalPrompt, historyMessages)
16733
+ : '';
16734
+ const systemPrompt = [
16735
+ '你是 codexmate 任务编排中的 OpenAI Chat-compatible 执行节点。',
16736
+ '基于用户给出的目标、前置节点摘要、同一 thread 历史和当前工作区快照,输出可执行、可验证、事实谨慎的结果。',
16737
+ allowWrite
16738
+ ? '当前任务允许写入;如果需要创建或更新文件,优先使用 fenced code block:```codexmate-file action="write" path="相对路径"。代码块内容会写入任务工作目录内对应文本文件。删除文件请单独写一行 CODEXMATE_DELETE_FILE: 相对路径。只允许安全相对路径。'
16739
+ : '当前任务是只读/验证节点,不要声称修改了文件。',
16740
+ '查询/读取文件时,先依据用户要求和工作区快照回答;如果快照不足,说明缺口,不要编造。',
16741
+ '回答使用中文,避免空泛总结。'
16742
+ ].join('\n');
16743
+ const currentUserMessage = [
16744
+ finalPrompt,
16745
+ workspaceContext ? '\n--- codexmate workspace snapshot ---' : '',
16746
+ workspaceContext
16747
+ ].filter(Boolean).join('\n');
16748
+ const startedAt = Date.now();
16749
+ const body = {
16750
+ model: requestConfig.model,
16751
+ messages: [
16752
+ { role: 'system', content: systemPrompt },
16753
+ ...historyMessages.map((message) => ({
16754
+ role: message.role === 'assistant' ? 'assistant' : 'user',
16755
+ content: String(message.content || '')
16756
+ })),
16757
+ { role: 'user', content: currentUserMessage }
16758
+ ],
16759
+ temperature: Number.isFinite(requestConfig.temperature) ? requestConfig.temperature : 0.2,
16760
+ stream: false
16154
16761
  };
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
- });
16762
+ const result = await postOpenAiChatCompletion(requestConfig, body, {
16763
+ registerAbort: context.registerAbort
16182
16764
  });
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);
16765
+ const text = result.ok ? extractModelResponseText(result.payload) : '';
16766
+ const success = result.ok && !!text;
16767
+ const errorMessage = success ? '' : (result.error || 'OpenAI Chat response did not contain text');
16768
+ const summary = truncateTaskText(text || errorMessage, 400);
16769
+ const safeEndpoint = redactTaskEndpointUrl(requestConfig.endpointUrl);
16770
+ const materialized = success && allowWrite
16771
+ ? materializeOpenAiChatTaskArtifacts(text, {
16772
+ cwd: context.cwd || process.cwd(),
16773
+ target: context.plan && context.plan.target ? context.plan.target : '',
16774
+ notes: context.plan && context.plan.notes ? context.plan.notes : '',
16775
+ prompt: node.prompt || ''
16776
+ })
16777
+ : { files: [], warnings: [] };
16778
+ const workspaceOperations = success && typeof applyWorkspaceFileOperations === 'function'
16779
+ ? applyWorkspaceFileOperations(text, context.cwd || process.cwd(), { allowWrite })
16780
+ : { files: [], warnings: [] };
16781
+ const threadAppend = threadId && typeof appendWorkspaceChatThread === 'function' && !!threadStoreDir
16782
+ ? appendWorkspaceChatThread(threadStoreDir, threadId, [
16783
+ { role: 'user', content: finalPrompt },
16784
+ { role: 'assistant', content: text || errorMessage }
16785
+ ], { cwd: context.cwd || process.cwd() })
16786
+ : { ok: false, error: 'thread id is empty' };
16196
16787
  return {
16197
16788
  success,
16198
16789
  error: errorMessage,
16199
16790
  summary,
16200
16791
  output: {
16201
- exitCode: exit.code,
16202
- signal: exit.signal || '',
16203
- sessionId,
16204
- lastMessage,
16205
- events: parsedEvents,
16206
- stdoutPreview: stdoutLines,
16207
- stderrPreview: stderrLines
16792
+ provider: requestConfig.providerName,
16793
+ model: requestConfig.model,
16794
+ endpoint: safeEndpoint,
16795
+ status: result.status || 0,
16796
+ text,
16797
+ response: result.payload || null,
16798
+ durationMs: Date.now() - startedAt,
16799
+ messageCount: body.messages.length,
16800
+ threadId,
16801
+ threadStore: threadAppend && threadAppend.file ? threadAppend.file : '',
16802
+ materializedFiles: materialized.files,
16803
+ workspaceFiles: workspaceOperations.files
16208
16804
  },
16209
16805
  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 }))
16806
+ { at: toIsoTime(Date.now()), level: 'info', message: `OpenAI Chat request provider=${requestConfig.providerName} model=${requestConfig.model} status=${result.status || 0}` },
16807
+ ...(threadId ? [{ at: toIsoTime(Date.now()), level: 'info', message: `workspace chat thread=${threadId} messages=${body.messages.length}` }] : []),
16808
+ ...(success ? [{ at: toIsoTime(Date.now()), level: 'info', message: truncateTaskText(text, 1200) }] : [{ at: toIsoTime(Date.now()), level: 'error', message: errorMessage }]),
16809
+ ...materialized.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `materialized artifact ${file.relativePath} (${file.bytes} bytes)` })),
16810
+ ...workspaceOperations.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `workspace ${file.operation} ${file.relativePath} (${file.bytes} bytes)` })),
16811
+ ...materialized.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })),
16812
+ ...workspaceOperations.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })),
16813
+ ...(threadAppend && threadAppend.error ? [{ at: toIsoTime(Date.now()), level: 'warn', message: threadAppend.error }] : [])
16212
16814
  ]
16213
16815
  };
16214
16816
  }
@@ -16246,7 +16848,7 @@ async function executeTaskNodeAdapter(node, context = {}) {
16246
16848
  : []
16247
16849
  };
16248
16850
  }
16249
- return runCodexExecTaskNode(node, context);
16851
+ return runOpenAiChatTaskNode(node, context);
16250
16852
  }
16251
16853
 
16252
16854
  async function runTaskPlanInternal(plan, options = {}) {
@@ -16260,13 +16862,18 @@ async function runTaskPlanInternal(plan, options = {}) {
16260
16862
  }
16261
16863
  const taskId = typeof options.taskId === 'string' && options.taskId.trim() ? options.taskId.trim() : (plan.id || createTaskId());
16262
16864
  const runId = typeof options.runId === 'string' && options.runId.trim() ? options.runId.trim() : createTaskRunId();
16865
+ const threadId = normalizeTaskThreadId(options.threadId || plan.threadId) || createTaskThreadId();
16866
+ plan.threadId = threadId;
16867
+ const cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd();
16263
16868
  const controller = new AbortController();
16264
16869
  const baseDetail = {
16265
16870
  runId,
16266
16871
  taskId,
16872
+ threadId,
16267
16873
  workerPid: process.pid,
16268
16874
  title: plan.title || '',
16269
16875
  target: plan.target || '',
16876
+ cwd,
16270
16877
  engine: normalizeTaskEngine(plan.engine),
16271
16878
  allowWrite: plan.allowWrite === true,
16272
16879
  dryRun: plan.dryRun === true,
@@ -16302,6 +16909,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16302
16909
  const queued = upsertTaskQueueItem({
16303
16910
  ...options.queueItem,
16304
16911
  taskId,
16912
+ threadId,
16913
+ cwd,
16305
16914
  status: 'running',
16306
16915
  runStatus: 'running',
16307
16916
  lastRunId: runId,
@@ -16320,9 +16929,10 @@ async function runTaskPlanInternal(plan, options = {}) {
16320
16929
  plan,
16321
16930
  taskId,
16322
16931
  runId,
16932
+ threadId,
16323
16933
  allowWrite: plan.allowWrite === true,
16324
16934
  dryRun: plan.dryRun === true,
16325
- cwd: plan.cwd || process.cwd()
16935
+ cwd
16326
16936
  }),
16327
16937
  onUpdate: async (snapshot) => {
16328
16938
  const nextDetail = {
@@ -16336,6 +16946,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16336
16946
  const queued = upsertTaskQueueItem({
16337
16947
  ...options.queueItem,
16338
16948
  taskId,
16949
+ threadId,
16950
+ cwd,
16339
16951
  status: snapshot.status === 'success'
16340
16952
  ? 'completed'
16341
16953
  : (snapshot.status === 'failed' ? 'failed' : (snapshot.status === 'cancelled' ? 'cancelled' : 'running')),
@@ -16365,6 +16977,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16365
16977
  const queued = upsertTaskQueueItem({
16366
16978
  ...options.queueItem,
16367
16979
  taskId,
16980
+ threadId,
16981
+ cwd,
16368
16982
  status: run.status === 'success'
16369
16983
  ? 'completed'
16370
16984
  : (run.status === 'cancelled' ? 'cancelled' : 'failed'),
@@ -16395,8 +17009,10 @@ function addTaskToQueue(params = {}) {
16395
17009
  const taskId = typeof params.taskId === 'string' && params.taskId.trim() ? params.taskId.trim() : createTaskId();
16396
17010
  const item = upsertTaskQueueItem({
16397
17011
  taskId,
17012
+ threadId: plan.threadId || createTaskThreadId(),
16398
17013
  title: plan.title,
16399
17014
  target: plan.target,
17015
+ cwd: plan.cwd || process.cwd(),
16400
17016
  status: 'queued',
16401
17017
  createdAt: toIsoTime(Date.now()),
16402
17018
  updatedAt: toIsoTime(Date.now()),