codexmate 0.0.55 → 0.0.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +2 -0
  2. package/README.vi.md +2 -0
  3. package/README.zh.md +2 -0
  4. package/cli/local-bridge.js +221 -22
  5. package/cli.js +797 -134
  6. package/lib/task-orchestrator.js +90 -21
  7. package/lib/task-workspace-chat.js +292 -0
  8. package/package.json +2 -2
  9. package/web-ui/app.js +57 -129
  10. package/web-ui/modules/app.computed.main-tabs.mjs +304 -7
  11. package/web-ui/modules/app.computed.session.mjs +210 -0
  12. package/web-ui/modules/app.methods.agents.mjs +3 -0
  13. package/web-ui/modules/app.methods.claude-config.mjs +178 -22
  14. package/web-ui/modules/app.methods.codex-config.mjs +294 -65
  15. package/web-ui/modules/app.methods.navigation.mjs +71 -84
  16. package/web-ui/modules/app.methods.openclaw-editing.mjs +3 -6
  17. package/web-ui/modules/app.methods.providers.mjs +15 -1
  18. package/web-ui/modules/app.methods.runtime.mjs +7 -2
  19. package/web-ui/modules/app.methods.session-actions.mjs +25 -12
  20. package/web-ui/modules/app.methods.session-browser.mjs +23 -54
  21. package/web-ui/modules/app.methods.session-trash.mjs +0 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +35 -17
  23. package/web-ui/modules/app.methods.task-orchestration.mjs +347 -8
  24. package/web-ui/modules/app.methods.tool-config-permissions.mjs +0 -3
  25. package/web-ui/modules/app.methods.web-ui-preferences.mjs +415 -68
  26. package/web-ui/modules/config-template-confirm-pref.mjs +2 -3
  27. package/web-ui/modules/i18n/locales/en.mjs +187 -28
  28. package/web-ui/modules/i18n/locales/ja.mjs +184 -25
  29. package/web-ui/modules/i18n/locales/vi.mjs +186 -33
  30. package/web-ui/modules/i18n/locales/zh-tw.mjs +187 -28
  31. package/web-ui/modules/i18n/locales/zh.mjs +189 -30
  32. package/web-ui/modules/i18n.mjs +5 -12
  33. package/web-ui/modules/provider-default-names.mjs +25 -0
  34. package/web-ui/modules/sessions-filters-url.mjs +1 -2
  35. package/web-ui/partials/index/layout-header.html +37 -14
  36. package/web-ui/partials/index/modal-health-check.html +69 -5
  37. package/web-ui/partials/index/panel-config-codex.html +2 -2
  38. package/web-ui/partials/index/panel-orchestration.html +489 -282
  39. package/web-ui/partials/index/panel-sessions.html +97 -17
  40. package/web-ui/res/web-ui-render.precompiled.js +1423 -732
  41. package/web-ui/session-helpers.mjs +4 -1
  42. package/web-ui/styles/layout-shell.css +157 -1
  43. package/web-ui/styles/navigation-panels.css +11 -0
  44. package/web-ui/styles/responsive.css +98 -0
  45. package/web-ui/styles/sessions-preview.css +212 -2
  46. package/web-ui/styles/sessions-toolbar-trash.css +61 -18
  47. package/web-ui/styles/skills-list.css +122 -0
  48. package/web-ui/styles/task-orchestration.css +2161 -4
  49. package/web-ui/styles/titles-cards.css +52 -0
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,
@@ -231,6 +238,15 @@ const PROVIDER_CACHE_FILE_GROUPS = Object.freeze({
231
238
  'opencode-provider-current-models.json'
232
239
  ]
233
240
  });
241
+ const PROVIDER_CACHE_PROVIDER_FILES = Object.freeze([
242
+ 'codex-providers.json',
243
+ 'claude-providers.json',
244
+ 'opencode-providers.json'
245
+ ]);
246
+ const PROVIDER_CACHE_CURRENT_MODEL_FILES = Object.freeze([
247
+ 'codex-provider-current-models.json',
248
+ 'opencode-provider-current-models.json'
249
+ ]);
234
250
  const PROVIDER_CACHE_MAX_FILE_BYTES = 256 * 1024;
235
251
  const CODEXMATE_PREFERENCES_FILE = path.join(CODEXMATE_DIR, 'preferences.json');
236
252
  const CODEXMATE_OPENCODE_DIR = path.join(CODEXMATE_DIR, 'opencode');
@@ -249,6 +265,9 @@ const TASK_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-task-runs.jsonl');
249
265
  const TASK_RUN_DETAILS_DIR = path.join(CONFIG_DIR, 'codexmate-task-runs');
250
266
  const TASK_QUEUE_WORKER_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue-worker.json');
251
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;
252
271
  const AUTOMATION_CONFIG_FILE = path.join(CONFIG_DIR, 'codexmate-automation.json');
253
272
  const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
254
273
  const DEFAULT_MODEL_CONTEXT_WINDOW = 190000;
@@ -981,6 +1000,65 @@ function normalizePromptsSubTabPreference(value) {
981
1000
  return normalized === 'claude-project' ? 'claude-project' : 'codex';
982
1001
  }
983
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
+
984
1062
  function normalizeWebUiPreferences(value = {}) {
985
1063
  const source = isPlainObject(value) ? value : {};
986
1064
  const navigation = isPlainObject(source.navigation) ? source.navigation : {};
@@ -993,6 +1071,18 @@ function normalizeWebUiPreferences(value = {}) {
993
1071
  sessionsUsageTimeRange: normalizeUsageTimeRangePreference(source.sessionsUsageTimeRange),
994
1072
  promptsSubTab: normalizePromptsSubTabPreference(source.promptsSubTab),
995
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),
996
1086
  navigation: {
997
1087
  mainTab: normalizeMainTabPreference(navigation.mainTab),
998
1088
  configMode: normalizeConfigModePreference(navigation.configMode),
@@ -1021,6 +1111,9 @@ function setWebUiPreferences(params = {}) {
1021
1111
  }
1022
1112
  });
1023
1113
  preferences.webUi = next;
1114
+ if (isPlainObject(incoming.toolConfigPermissions)) {
1115
+ preferences.toolConfigPermissions = normalizeToolConfigPermissions(incoming.toolConfigPermissions);
1116
+ }
1024
1117
  writeCodexmatePreferences(preferences);
1025
1118
  return { success: true, preferences: next };
1026
1119
  }
@@ -1083,7 +1176,8 @@ function getApiToolConfigWriteTarget(action) {
1083
1176
  'restore-claude-dir',
1084
1177
  'claude-local-bridge-toggle',
1085
1178
  'claude-local-bridge-set-excluded',
1086
- 'claude-local-bridge-sync-providers'
1179
+ 'claude-local-bridge-sync-providers',
1180
+ 'delete-provider-cache-record'
1087
1181
  ]);
1088
1182
  const opencodeWriteActions = new Set([
1089
1183
  'apply-opencode-config',
@@ -2877,6 +2971,99 @@ function normalizeProviderCacheProviderMap(rawProviders) {
2877
2971
  return providers;
2878
2972
  }
2879
2973
 
2974
+ function removeProviderFromProviderCacheContainer(rawProviders, providerName) {
2975
+ const targetName = typeof providerName === 'string' ? providerName.trim() : '';
2976
+ if (!targetName) return { value: rawProviders, changed: false };
2977
+
2978
+ const matchesProviderName = (name) => String(name || '').trim() === targetName;
2979
+ if (Array.isArray(rawProviders)) {
2980
+ const filtered = rawProviders.filter((item) => {
2981
+ if (!isPlainObject(item)) return true;
2982
+ const itemName = pickProviderCacheString(item, ['name', 'id', 'provider']);
2983
+ return !matchesProviderName(itemName);
2984
+ });
2985
+ return { value: filtered, changed: filtered.length !== rawProviders.length };
2986
+ }
2987
+ if (!isPlainObject(rawProviders)) return { value: rawProviders, changed: false };
2988
+
2989
+ const next = { ...rawProviders };
2990
+ let changed = false;
2991
+ for (const [name, entry] of Object.entries(rawProviders)) {
2992
+ const entryName = isPlainObject(entry)
2993
+ ? pickProviderCacheString(entry, ['name', 'id', 'provider'])
2994
+ : '';
2995
+ if (matchesProviderName(name) || matchesProviderName(entryName)) {
2996
+ delete next[name];
2997
+ changed = true;
2998
+ }
2999
+ }
3000
+ return { value: next, changed };
3001
+ }
3002
+
3003
+ function resolveProviderCacheDeleteGroups(groups) {
3004
+ const requested = Array.isArray(groups) ? groups : (groups ? [groups] : []);
3005
+ const normalized = requested
3006
+ .map((item) => String(item || '').trim().toLowerCase())
3007
+ .filter((item) => Object.prototype.hasOwnProperty.call(PROVIDER_CACHE_FILE_GROUPS, item));
3008
+ return normalized.length ? Array.from(new Set(normalized)) : Object.keys(PROVIDER_CACHE_FILE_GROUPS);
3009
+ }
3010
+
3011
+ function removeProviderFromProviderCacheRecords(providerName, options = {}) {
3012
+ const targetName = typeof providerName === 'string' ? providerName.trim() : '';
3013
+ const summary = { removed: false, providerFiles: [], currentModelFiles: [] };
3014
+ if (!targetName) return summary;
3015
+
3016
+ const groups = resolveProviderCacheDeleteGroups(options.groups || options.group);
3017
+ const providerFiles = groups
3018
+ .flatMap((group) => PROVIDER_CACHE_FILE_GROUPS[group] || [])
3019
+ .filter((fileName) => PROVIDER_CACHE_PROVIDER_FILES.includes(fileName));
3020
+ const currentModelFiles = groups
3021
+ .flatMap((group) => PROVIDER_CACHE_FILE_GROUPS[group] || [])
3022
+ .filter((fileName) => PROVIDER_CACHE_CURRENT_MODEL_FILES.includes(fileName));
3023
+
3024
+ for (const fileName of Array.from(new Set(providerFiles))) {
3025
+ const existing = readProviderCacheJsonObject(fileName);
3026
+ if (!isPlainObject(existing) || !Object.prototype.hasOwnProperty.call(existing, 'providers')) continue;
3027
+ const removed = removeProviderFromProviderCacheContainer(existing.providers, targetName);
3028
+ if (!removed.changed) continue;
3029
+ writeProviderCacheJsonObject(fileName, {
3030
+ ...existing,
3031
+ generatedAt: new Date().toISOString(),
3032
+ providers: removed.value
3033
+ });
3034
+ summary.removed = true;
3035
+ summary.providerFiles.push(fileName);
3036
+ }
3037
+
3038
+ for (const fileName of Array.from(new Set(currentModelFiles))) {
3039
+ const existing = readProviderCacheJsonObject(fileName);
3040
+ if (!isPlainObject(existing) || !Object.prototype.hasOwnProperty.call(existing, targetName)) continue;
3041
+ const next = { ...existing };
3042
+ delete next[targetName];
3043
+ writeProviderCacheJsonObject(fileName, next);
3044
+ summary.removed = true;
3045
+ summary.currentModelFiles.push(fileName);
3046
+ }
3047
+ return summary;
3048
+ }
3049
+
3050
+ function deleteProviderCacheRecord(params = {}) {
3051
+ const name = typeof params.name === 'string' ? params.name.trim() : '';
3052
+ if (!name) return { error: '名称不能为空' };
3053
+ const group = typeof params.group === 'string' ? params.group.trim().toLowerCase() : '';
3054
+ const groups = resolveProviderCacheDeleteGroups(group || params.groups);
3055
+ const summary = removeProviderFromProviderCacheRecords(name, { groups });
3056
+ return {
3057
+ success: true,
3058
+ name,
3059
+ groups,
3060
+ removed: summary.removed,
3061
+ providerFiles: summary.providerFiles,
3062
+ currentModelFiles: summary.currentModelFiles,
3063
+ records: readProviderCacheRecords()
3064
+ };
3065
+ }
3066
+
2880
3067
  function readClaudeProviderCacheProvider(name) {
2881
3068
  const targetName = typeof name === 'string' ? name.trim() : '';
2882
3069
  if (!targetName) return null;
@@ -2953,10 +3140,11 @@ function buildProviderCacheSyncProviders() {
2953
3140
  function mergeProviderCacheFile(fileName, nextProviders, buildEntry) {
2954
3141
  const existing = readProviderCacheJsonObject(fileName);
2955
3142
  const existingProviders = normalizeProviderCacheProviderMap(existing.providers);
2956
- const providers = { ...existingProviders };
3143
+ const providers = {};
2957
3144
  for (const provider of nextProviders) {
2958
3145
  const previous = isPlainObject(providers[provider.name]) ? providers[provider.name] : {};
2959
- providers[provider.name] = { ...previous, ...buildEntry(provider) };
3146
+ const cachedPrevious = isPlainObject(existingProviders[provider.name]) ? existingProviders[provider.name] : previous;
3147
+ providers[provider.name] = { ...cachedPrevious, ...buildEntry(provider) };
2960
3148
  }
2961
3149
  const next = {
2962
3150
  ...existing,
@@ -2970,9 +3158,13 @@ function mergeProviderCacheFile(fileName, nextProviders, buildEntry) {
2970
3158
 
2971
3159
  function mergeProviderCacheCurrentModelsFile(fileName, nextProviders) {
2972
3160
  const existing = readProviderCacheJsonObject(fileName);
2973
- const next = { ...existing };
3161
+ const next = {};
2974
3162
  for (const provider of nextProviders) {
2975
- if (provider.model) next[provider.name] = provider.model;
3163
+ if (provider.model) {
3164
+ next[provider.name] = provider.model;
3165
+ } else if (typeof existing[provider.name] === 'string' && existing[provider.name].trim()) {
3166
+ next[provider.name] = existing[provider.name];
3167
+ }
2976
3168
  }
2977
3169
  const displayPath = writeProviderCacheJsonObject(fileName, next);
2978
3170
  return { path: displayPath, modelCount: Object.keys(next).length };
@@ -3213,6 +3405,7 @@ function performProviderDeletion(name, options = {}) {
3213
3405
 
3214
3406
  writeCurrentModels(currentModels);
3215
3407
  writeConfig(updatedContent.trimEnd() + lineEnding);
3408
+ removeProviderFromProviderCacheRecords(name);
3216
3409
 
3217
3410
  return result;
3218
3411
  }
@@ -12387,6 +12580,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
12387
12580
  case 'sync-provider-cache-records':
12388
12581
  result = syncProviderCacheRecords();
12389
12582
  break;
12583
+ case 'delete-provider-cache-record':
12584
+ result = deleteProviderCacheRecord(params || {});
12585
+ break;
12390
12586
  case 'delete-provider':
12391
12587
  result = deleteProviderFromConfig(params || {});
12392
12588
  break;
@@ -12903,6 +13099,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
12903
13099
  detached: true,
12904
13100
  taskId,
12905
13101
  runId,
13102
+ threadId: plan.threadId || '',
13103
+ cwd: plan.cwd || '',
12906
13104
  warnings: validation.warnings || []
12907
13105
  };
12908
13106
  } else {
@@ -13747,7 +13945,11 @@ function printTaskHelp() {
13747
13945
  console.log(' --allow-write 允许写入工作区');
13748
13946
  console.log(' --dry-run 仅计划/预演,不执行写入');
13749
13947
  console.log(' --plan-only 仅输出计划,不执行');
13750
- 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(兼容别名)');
13751
13953
  console.log(' --concurrency <N> 并发度');
13752
13954
  console.log(' --auto-fix-rounds <N> 自动修复回合数');
13753
13955
  console.log(' --limit <N> runs/queue list 数量');
@@ -13770,7 +13972,7 @@ function parseTaskCliOptions(args = []) {
13770
13972
  allowWrite: false,
13771
13973
  dryRun: false,
13772
13974
  planOnly: false,
13773
- engine: 'codex',
13975
+ engine: 'openai-chat',
13774
13976
  concurrency: 2,
13775
13977
  autoFixRounds: 1,
13776
13978
  limit: 20,
@@ -13917,6 +14119,38 @@ function parseTaskCliOptions(args = []) {
13917
14119
  options.explicit.autoFixRounds = true;
13918
14120
  continue;
13919
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
+ }
13920
14154
  if (arg === '--limit') {
13921
14155
  const value = parseInt(args[i + 1], 10);
13922
14156
  if (Number.isFinite(value)) options.limit = value;
@@ -13974,9 +14208,11 @@ function buildTaskCliPayload(options = {}, rest = []) {
13974
14208
  if (explicit.followUps && Array.isArray(options.followUps)) payload.followUps = options.followUps.slice();
13975
14209
  if (explicit.allowWrite) payload.allowWrite = options.allowWrite === true;
13976
14210
  if (explicit.dryRun) payload.dryRun = options.dryRun === true;
13977
- if (explicit.engine) payload.engine = options.engine || 'codex';
14211
+ if (explicit.engine) payload.engine = options.engine || 'openai-chat';
13978
14212
  if (explicit.concurrency) payload.concurrency = options.concurrency;
13979
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;
13980
14216
  if (explicit.taskId && options.taskId) payload.taskId = options.taskId;
13981
14217
  if (explicit.runId && options.runId) payload.runId = options.runId;
13982
14218
  if (!payload.target && Array.isArray(rest) && rest.length > 0) {
@@ -13990,7 +14226,7 @@ function buildTaskCliPayload(options = {}, rest = []) {
13990
14226
 
13991
14227
  function printTaskPlanSummary(plan, warnings = []) {
13992
14228
  console.log(`\n任务计划: ${plan.title || '(untitled)'}`);
13993
- console.log(` engine: ${plan.engine || 'codex'}`);
14229
+ console.log(` engine: ${plan.engine || 'openai-chat'}`);
13994
14230
  console.log(` allowWrite: ${plan.allowWrite === true ? 'yes' : 'no'}`);
13995
14231
  console.log(` dryRun: ${plan.dryRun === true ? 'yes' : 'no'}`);
13996
14232
  console.log(` concurrency: ${plan.concurrency || 1}`);
@@ -15444,6 +15680,10 @@ function createTaskRunId() {
15444
15680
  return `tr-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
15445
15681
  }
15446
15682
 
15683
+ function createTaskThreadId() {
15684
+ return `tt-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
15685
+ }
15686
+
15447
15687
  function validateTaskRunId(value) {
15448
15688
  const runId = typeof value === 'string' ? value.trim() : '';
15449
15689
  if (!runId) {
@@ -15455,9 +15695,19 @@ function validateTaskRunId(value) {
15455
15695
  return { ok: true, error: '', runId };
15456
15696
  }
15457
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
+
15458
15706
  function normalizeTaskEngine(value) {
15459
- const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
15460
- 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';
15461
15711
  }
15462
15712
 
15463
15713
  function normalizeTaskFollowUps(input = []) {
@@ -15498,6 +15748,7 @@ function normalizeTaskPlanRequest(params = {}) {
15498
15748
  : (typeof source.followUp === 'string' && source.followUp.trim() ? [source.followUp.trim()] : []);
15499
15749
  return {
15500
15750
  id: typeof source.id === 'string' ? source.id.trim() : '',
15751
+ threadId: normalizeTaskThreadId(source.threadId || source.conversationId || source.sessionId),
15501
15752
  title: typeof source.title === 'string' ? source.title.trim() : '',
15502
15753
  target: typeof source.target === 'string' ? source.target.trim() : '',
15503
15754
  notes: typeof source.notes === 'string' ? source.notes.trim() : '',
@@ -15507,6 +15758,7 @@ function normalizeTaskPlanRequest(params = {}) {
15507
15758
  dryRun: source.dryRun === true,
15508
15759
  concurrency: Number.isFinite(source.concurrency) ? source.concurrency : parseInt(source.concurrency, 10),
15509
15760
  autoFixRounds: Number.isFinite(source.autoFixRounds) ? source.autoFixRounds : parseInt(source.autoFixRounds, 10),
15761
+ previewOnly: source.previewOnly === true,
15510
15762
  workflowIds: rawWorkflowIds,
15511
15763
  followUps: normalizeTaskFollowUps(rawFollowUps)
15512
15764
  };
@@ -15515,12 +15767,18 @@ function normalizeTaskPlanRequest(params = {}) {
15515
15767
  function coerceTaskPlanPayload(params = {}) {
15516
15768
  if (params && params.plan && typeof params.plan === 'object' && !Array.isArray(params.plan)) {
15517
15769
  const plan = cloneJson(params.plan, {});
15518
- 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'];
15519
15771
  for (const key of overrideKeys) {
15520
15772
  if (Object.prototype.hasOwnProperty.call(params, key) && params[key] !== undefined) {
15521
- 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
+ }
15522
15778
  }
15523
15779
  }
15780
+ plan.cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd();
15781
+ plan.threadId = normalizeTaskThreadId(plan.threadId) || createTaskThreadId();
15524
15782
  plan.engine = normalizeTaskEngine(plan.engine);
15525
15783
  plan.workflowIds = normalizeTaskFollowUps(plan.workflowIds || []).map((id) => normalizeWorkflowId(id)).filter(Boolean);
15526
15784
  plan.followUps = normalizeTaskFollowUps(plan.followUps || []);
@@ -15535,6 +15793,7 @@ function coerceTaskPlanPayload(params = {}) {
15535
15793
  });
15536
15794
  return {
15537
15795
  ...plan,
15796
+ threadId: request.threadId || createTaskThreadId(),
15538
15797
  engine: normalizeTaskEngine(request.engine || plan.engine)
15539
15798
  };
15540
15799
  }
@@ -15557,8 +15816,10 @@ function normalizeTaskQueueItem(raw = {}) {
15557
15816
  const taskId = typeof raw.taskId === 'string' ? raw.taskId.trim() : '';
15558
15817
  return {
15559
15818
  taskId: taskId || createTaskId(),
15819
+ threadId: normalizeTaskThreadId(raw.threadId || plan.threadId) || createTaskThreadId(),
15560
15820
  title: typeof raw.title === 'string' ? raw.title.trim() : (typeof plan.title === 'string' ? plan.title.trim() : ''),
15561
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() : ''),
15562
15823
  status: typeof raw.status === 'string' ? raw.status.trim().toLowerCase() : 'queued',
15563
15824
  createdAt: toIsoTime(raw.createdAt || Date.now(), ''),
15564
15825
  updatedAt: toIsoTime(raw.updatedAt || raw.createdAt || Date.now(), ''),
@@ -15759,8 +16020,10 @@ function collectTaskRunSummary(detail = {}) {
15759
16020
  return {
15760
16021
  runId: detail.runId || '',
15761
16022
  taskId: detail.taskId || '',
16023
+ threadId: detail.threadId || '',
15762
16024
  title: detail.title || '',
15763
16025
  target: detail.target || '',
16026
+ cwd: detail.cwd || '',
15764
16027
  engine: detail.engine || '',
15765
16028
  allowWrite: detail.allowWrite === true,
15766
16029
  dryRun: detail.dryRun === true,
@@ -15884,6 +16147,7 @@ function buildTaskOverviewPayload(options = {}) {
15884
16147
  }
15885
16148
  return {
15886
16149
  workflows: workflowCatalog.workflows,
16150
+ openAiChatStatus: buildTaskOpenAiChatStatus(),
15887
16151
  warnings,
15888
16152
  queue,
15889
16153
  runs,
@@ -15946,20 +16210,434 @@ function readCodexLastMessageFile(filePath) {
15946
16210
  }
15947
16211
  }
15948
16212
 
15949
- async function runCodexExecTaskNode(node, context = {}) {
15950
- const codexPath = resolveSpawnCommand('codex');
15951
- const codexProbeCommand = process.platform === 'win32' ? 'codex' : codexPath;
15952
- 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) {
15953
16608
  return {
15954
16609
  success: false,
15955
- error: '未找到 codex CLI,请先安装并确保 PATH 可用',
15956
- summary: 'codex CLI 不可用',
16610
+ error: requestConfig.error,
16611
+ summary: requestConfig.error,
15957
16612
  output: null,
15958
- logs: [{ at: toIsoTime(Date.now()), level: 'error', message: 'codex CLI 不可用' }]
16613
+ logs: [{ at: toIsoTime(Date.now()), level: 'error', message: requestConfig.error }]
15959
16614
  };
15960
16615
  }
15961
- const allowWrite = context.allowWrite === true && node.write === true;
15962
- const cwd = typeof context.cwd === 'string' && context.cwd.trim() ? context.cwd.trim() : process.cwd();
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 }]
16638
+ };
16639
+ }
16640
+ const allowWrite = context.allowWrite === true && node.write !== false && context.dryRun !== true;
15963
16641
  const dependencyResults = Array.isArray(context.dependencyResults) ? context.dependencyResults : [];
15964
16642
  const dependencyLines = dependencyResults
15965
16643
  .map((item) => {
@@ -15979,124 +16657,95 @@ async function runCodexExecTaskNode(node, context = {}) {
15979
16657
  promptParts.push('请在保持目标不变的前提下修复上一轮失败并继续完成当前节点。');
15980
16658
  }
15981
16659
  const finalPrompt = promptParts.filter(Boolean).join('\n\n');
15982
- const tempRoot = path.join(TASK_RUN_DETAILS_DIR, 'tmp');
15983
- ensureDir(tempRoot);
15984
- const tempDir = fs.mkdtempSync(path.join(tempRoot, 'codex-'));
15985
- const outputFile = path.join(tempDir, 'last-message.txt');
15986
- const args = [
15987
- '-a', 'never',
15988
- '-s', allowWrite ? 'workspace-write' : 'read-only',
15989
- '-C', cwd,
15990
- 'exec',
15991
- '--json',
15992
- '--skip-git-repo-check',
15993
- '--output-last-message', outputFile,
15994
- finalPrompt
15995
- ];
15996
- const stdoutLines = [];
15997
- const stderrLines = [];
15998
- const parsedEvents = [];
15999
- let sessionId = '';
16000
- let stdoutPartial = '';
16001
- let stderrPartial = '';
16002
- const processCapturedLine = (bucket, line) => {
16003
- const normalizedLine = String(line || '').trim();
16004
- if (!normalizedLine) {
16005
- return;
16006
- }
16007
- if (bucket.length < 120) {
16008
- bucket.push(truncateTaskText(normalizedLine, 1200));
16009
- }
16010
- try {
16011
- const payload = JSON.parse(normalizedLine);
16012
- if (parsedEvents.length < 120) {
16013
- parsedEvents.push(payload);
16014
- }
16015
- if (!sessionId) {
16016
- sessionId = findCodexSessionId(payload);
16017
- }
16018
- } catch (_) { }
16019
- };
16020
- const captureLines = (bucket, text, stream) => {
16021
- const currentPartial = stream === 'stderr' ? stderrPartial : stdoutPartial;
16022
- const merged = `${currentPartial}${String(text || '')}`;
16023
- const pieces = merged.split(/\r?\n/g);
16024
- const nextPartial = pieces.pop() || '';
16025
- if (stream === 'stderr') {
16026
- stderrPartial = nextPartial;
16027
- } else {
16028
- stdoutPartial = nextPartial;
16029
- }
16030
- for (const line of pieces) {
16031
- processCapturedLine(bucket, line);
16032
- }
16033
- };
16034
- const flushCapturedPartial = (bucket, stream) => {
16035
- const partial = stream === 'stderr' ? stderrPartial : stdoutPartial;
16036
- if (stream === 'stderr') {
16037
- stderrPartial = '';
16038
- } else {
16039
- stdoutPartial = '';
16040
- }
16041
- 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
16042
16696
  };
16043
- const exit = await new Promise((resolve) => {
16044
- const child = spawn(codexPath, args, {
16045
- stdio: ['ignore', 'pipe', 'pipe'],
16046
- windowsHide: true,
16047
- shell: process.platform === 'win32'
16048
- });
16049
- if (typeof context.registerAbort === 'function') {
16050
- context.registerAbort(() => {
16051
- try {
16052
- child.kill('SIGTERM');
16053
- } catch (_) { }
16054
- });
16055
- }
16056
- child.stdout.on('data', (chunk) => {
16057
- captureLines(stdoutLines, chunk, 'stdout');
16058
- });
16059
- child.stderr.on('data', (chunk) => {
16060
- captureLines(stderrLines, chunk, 'stderr');
16061
- });
16062
- child.on('error', (error) => {
16063
- resolve({ code: 1, signal: '', error: error && error.message ? error.message : String(error || 'spawn failed') });
16064
- });
16065
- child.on('close', (code, signal) => {
16066
- flushCapturedPartial(stdoutLines, 'stdout');
16067
- flushCapturedPartial(stderrLines, 'stderr');
16068
- resolve({ code: typeof code === 'number' ? code : 1, signal: signal || '', error: '' });
16069
- });
16697
+ const result = await postOpenAiChatCompletion(requestConfig, body, {
16698
+ registerAbort: context.registerAbort
16070
16699
  });
16071
- const lastMessage = readCodexLastMessageFile(outputFile);
16072
- try {
16073
- if (fs.rmSync) {
16074
- fs.rmSync(tempDir, { recursive: true, force: true });
16075
- } else {
16076
- fs.rmdirSync(tempDir, { recursive: true });
16077
- }
16078
- } catch (_) { }
16079
- const success = exit.code === 0;
16080
- const errorMessage = success
16081
- ? ''
16082
- : (exit.error || stderrLines[stderrLines.length - 1] || stdoutLines[stdoutLines.length - 1] || `codex exec exited with code ${exit.code}`);
16083
- 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' };
16084
16722
  return {
16085
16723
  success,
16086
16724
  error: errorMessage,
16087
16725
  summary,
16088
16726
  output: {
16089
- exitCode: exit.code,
16090
- signal: exit.signal || '',
16091
- sessionId,
16092
- lastMessage,
16093
- events: parsedEvents,
16094
- stdoutPreview: stdoutLines,
16095
- 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
16096
16739
  },
16097
16740
  logs: [
16098
- ...stdoutLines.map((line) => ({ at: toIsoTime(Date.now()), level: 'info', message: line })),
16099
- ...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 }] : [])
16100
16749
  ]
16101
16750
  };
16102
16751
  }
@@ -16134,7 +16783,7 @@ async function executeTaskNodeAdapter(node, context = {}) {
16134
16783
  : []
16135
16784
  };
16136
16785
  }
16137
- return runCodexExecTaskNode(node, context);
16786
+ return runOpenAiChatTaskNode(node, context);
16138
16787
  }
16139
16788
 
16140
16789
  async function runTaskPlanInternal(plan, options = {}) {
@@ -16148,13 +16797,18 @@ async function runTaskPlanInternal(plan, options = {}) {
16148
16797
  }
16149
16798
  const taskId = typeof options.taskId === 'string' && options.taskId.trim() ? options.taskId.trim() : (plan.id || createTaskId());
16150
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();
16151
16803
  const controller = new AbortController();
16152
16804
  const baseDetail = {
16153
16805
  runId,
16154
16806
  taskId,
16807
+ threadId,
16155
16808
  workerPid: process.pid,
16156
16809
  title: plan.title || '',
16157
16810
  target: plan.target || '',
16811
+ cwd,
16158
16812
  engine: normalizeTaskEngine(plan.engine),
16159
16813
  allowWrite: plan.allowWrite === true,
16160
16814
  dryRun: plan.dryRun === true,
@@ -16190,6 +16844,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16190
16844
  const queued = upsertTaskQueueItem({
16191
16845
  ...options.queueItem,
16192
16846
  taskId,
16847
+ threadId,
16848
+ cwd,
16193
16849
  status: 'running',
16194
16850
  runStatus: 'running',
16195
16851
  lastRunId: runId,
@@ -16208,9 +16864,10 @@ async function runTaskPlanInternal(plan, options = {}) {
16208
16864
  plan,
16209
16865
  taskId,
16210
16866
  runId,
16867
+ threadId,
16211
16868
  allowWrite: plan.allowWrite === true,
16212
16869
  dryRun: plan.dryRun === true,
16213
- cwd: plan.cwd || process.cwd()
16870
+ cwd
16214
16871
  }),
16215
16872
  onUpdate: async (snapshot) => {
16216
16873
  const nextDetail = {
@@ -16224,6 +16881,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16224
16881
  const queued = upsertTaskQueueItem({
16225
16882
  ...options.queueItem,
16226
16883
  taskId,
16884
+ threadId,
16885
+ cwd,
16227
16886
  status: snapshot.status === 'success'
16228
16887
  ? 'completed'
16229
16888
  : (snapshot.status === 'failed' ? 'failed' : (snapshot.status === 'cancelled' ? 'cancelled' : 'running')),
@@ -16253,6 +16912,8 @@ async function runTaskPlanInternal(plan, options = {}) {
16253
16912
  const queued = upsertTaskQueueItem({
16254
16913
  ...options.queueItem,
16255
16914
  taskId,
16915
+ threadId,
16916
+ cwd,
16256
16917
  status: run.status === 'success'
16257
16918
  ? 'completed'
16258
16919
  : (run.status === 'cancelled' ? 'cancelled' : 'failed'),
@@ -16283,8 +16944,10 @@ function addTaskToQueue(params = {}) {
16283
16944
  const taskId = typeof params.taskId === 'string' && params.taskId.trim() ? params.taskId.trim() : createTaskId();
16284
16945
  const item = upsertTaskQueueItem({
16285
16946
  taskId,
16947
+ threadId: plan.threadId || createTaskThreadId(),
16286
16948
  title: plan.title,
16287
16949
  target: plan.target,
16950
+ cwd: plan.cwd || process.cwd(),
16288
16951
  status: 'queued',
16289
16952
  createdAt: toIsoTime(Date.now()),
16290
16953
  updatedAt: toIsoTime(Date.now()),