codexmate 0.0.37 → 0.0.39

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 (38) hide show
  1. package/cli/analytics-export-args.js +68 -0
  2. package/cli/builtin-proxy.js +626 -207
  3. package/cli/openai-bridge.js +541 -210
  4. package/cli/session-usage.js +187 -1
  5. package/cli.js +84 -2
  6. package/package.json +1 -1
  7. package/web-ui/app.js +12 -3
  8. package/web-ui/modules/app.computed.main-tabs.mjs +37 -30
  9. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  10. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  11. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  12. package/web-ui/modules/app.methods.providers.mjs +36 -10
  13. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  14. package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
  15. package/web-ui/modules/config-mode.computed.mjs +3 -3
  16. package/web-ui/modules/i18n.dict.mjs +13 -0
  17. package/web-ui/modules/i18n.mjs +65 -16
  18. package/web-ui/modules/skills.methods.mjs +1 -1
  19. package/web-ui/partials/index/layout-header.html +16 -46
  20. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  21. package/web-ui/partials/index/modal-webhook.html +8 -8
  22. package/web-ui/partials/index/modals-basic.html +56 -16
  23. package/web-ui/partials/index/panel-config-claude.html +20 -20
  24. package/web-ui/partials/index/panel-config-codex.html +5 -5
  25. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  26. package/web-ui/partials/index/panel-dashboard.html +62 -77
  27. package/web-ui/partials/index/panel-settings.html +28 -7
  28. package/web-ui/partials/index/panel-trash.html +14 -14
  29. package/web-ui/res/web-ui-render.precompiled.js +846 -539
  30. package/web-ui/styles/controls-forms.css +6 -0
  31. package/web-ui/styles/dashboard.css +46 -14
  32. package/web-ui/styles/layout-shell.css +45 -0
  33. package/web-ui/styles/navigation-panels.css +3 -3
  34. package/web-ui/styles/openclaw-structured.css +383 -33
  35. package/web-ui/styles/responsive.css +68 -0
  36. package/web-ui/styles/sessions-usage.css +105 -9
  37. package/web-ui/styles/settings-panel.css +4 -0
  38. package/web-ui/partials/index/panel-config-codex.html.bak +0 -337
@@ -113,6 +113,192 @@ async function listSessionUsageCore(params = {}, deps = {}) {
113
113
  return normalizedSessions.filter(Boolean);
114
114
  }
115
115
 
116
+ function readNonNegativeInteger(value) {
117
+ const numeric = Number(value);
118
+ if (!Number.isFinite(numeric) || numeric < 0) {
119
+ return 0;
120
+ }
121
+ return Math.floor(numeric);
122
+ }
123
+
124
+ function parseUsageExportDate(value, boundary) {
125
+ if (value === undefined || value === null || value === '') {
126
+ return null;
127
+ }
128
+ if (value instanceof Date) {
129
+ const time = value.getTime();
130
+ return Number.isFinite(time) ? time : NaN;
131
+ }
132
+ const raw = String(value).trim();
133
+ if (!raw) {
134
+ return null;
135
+ }
136
+ const dateOnly = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
137
+ if (dateOnly) {
138
+ const year = Number(dateOnly[1]);
139
+ const month = Number(dateOnly[2]) - 1;
140
+ const day = Number(dateOnly[3]);
141
+ const start = Date.UTC(year, month, day);
142
+ const normalized = new Date(start);
143
+ if (!Number.isFinite(start)
144
+ || normalized.getUTCFullYear() !== year
145
+ || normalized.getUTCMonth() !== month
146
+ || normalized.getUTCDate() !== day) {
147
+ return NaN;
148
+ }
149
+ return boundary === 'end' ? start + 24 * 60 * 60 * 1000 : start;
150
+ }
151
+ const parsed = Date.parse(raw);
152
+ return Number.isFinite(parsed) ? parsed : NaN;
153
+ }
154
+
155
+ function formatUsageExportDay(timestamp) {
156
+ return new Date(timestamp).toISOString().slice(0, 10);
157
+ }
158
+
159
+ function normalizeUsageExportFormat(value) {
160
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
161
+ return normalized === 'json' ? 'json' : 'csv';
162
+ }
163
+
164
+ function normalizeUsageExportModelFilters(params = {}) {
165
+ const raw = [];
166
+ const push = (value) => {
167
+ if (Array.isArray(value)) {
168
+ value.forEach(push);
169
+ return;
170
+ }
171
+ if (typeof value !== 'string') {
172
+ return;
173
+ }
174
+ value.split(',').forEach((item) => {
175
+ const normalized = item.trim().toLowerCase();
176
+ if (normalized) raw.push(normalized);
177
+ });
178
+ };
179
+ push(params.model);
180
+ push(params.models);
181
+ // API-facing alias: callers may pass modelType when they reuse usage filters
182
+ // outside the CLI flag surface.
183
+ push(params.modelType);
184
+ return [...new Set(raw)];
185
+ }
186
+
187
+ function sessionMatchesUsageExportModelFilters(session, filters) {
188
+ if (!filters.length) {
189
+ return true;
190
+ }
191
+ const models = [];
192
+ if (typeof session.model === 'string') models.push(session.model);
193
+ if (Array.isArray(session.models)) models.push(...session.models.filter(item => typeof item === 'string'));
194
+ const normalizedModels = models.map(item => item.trim().toLowerCase()).filter(Boolean);
195
+ return filters.some(filter => normalizedModels.some(model => model === filter || model.includes(filter)));
196
+ }
197
+
198
+ function escapeUsageCsvCell(value) {
199
+ const raw = value === undefined || value === null ? '' : String(value);
200
+ if (!/[",\n\r]/.test(raw)) {
201
+ return raw;
202
+ }
203
+ return `"${raw.replace(/"/g, '""')}"`;
204
+ }
205
+
206
+ function serializeUsageExportRowsToCsv(rows) {
207
+ const columns = ['date', 'model', 'tokens', 'sessions'];
208
+ const lines = [columns.join(',')];
209
+ for (const row of rows) {
210
+ lines.push(columns.map(column => escapeUsageCsvCell(row[column])).join(','));
211
+ }
212
+ return lines.join('\r\n') + '\r\n';
213
+ }
214
+
215
+ function buildUsageExportRows(sessions = [], params = {}) {
216
+ const fromTime = parseUsageExportDate(params.from ?? params.startDate, 'start');
217
+ const toTime = parseUsageExportDate(params.to ?? params.endDate, 'end');
218
+ if (Number.isNaN(fromTime)) {
219
+ return { error: 'Invalid from date' };
220
+ }
221
+ if (Number.isNaN(toTime)) {
222
+ return { error: 'Invalid to date' };
223
+ }
224
+ if (fromTime !== null && toTime !== null && fromTime >= toTime) {
225
+ return { error: 'from date must be before to date' };
226
+ }
227
+
228
+ const modelFilters = normalizeUsageExportModelFilters(params);
229
+ const groups = new Map();
230
+ for (const session of Array.isArray(sessions) ? sessions : []) {
231
+ if (!session || typeof session !== 'object' || Array.isArray(session)) {
232
+ continue;
233
+ }
234
+ if (!sessionMatchesUsageExportModelFilters(session, modelFilters)) {
235
+ continue;
236
+ }
237
+ const timestamp = Date.parse(session.updatedAt || session.createdAt || '');
238
+ if (!Number.isFinite(timestamp)) {
239
+ continue;
240
+ }
241
+ if (fromTime !== null && timestamp < fromTime) {
242
+ continue;
243
+ }
244
+ if (toTime !== null && timestamp >= toTime) {
245
+ continue;
246
+ }
247
+ const model = typeof session.model === 'string' && session.model.trim()
248
+ ? session.model.trim()
249
+ : (Array.isArray(session.models) && typeof session.models[0] === 'string' ? session.models[0].trim() : 'unknown');
250
+ if (!model) {
251
+ continue;
252
+ }
253
+ const date = formatUsageExportDay(timestamp);
254
+ const key = `${date}\u0000${model}`;
255
+ const current = groups.get(key) || { date, model, tokens: 0, sessions: 0 };
256
+ current.tokens += readNonNegativeInteger(session.totalTokens ?? session.tokens);
257
+ current.sessions += 1;
258
+ groups.set(key, current);
259
+ }
260
+
261
+ const rows = [...groups.values()].sort((a, b) => {
262
+ const dateCompare = a.date.localeCompare(b.date);
263
+ if (dateCompare !== 0) return dateCompare;
264
+ return a.model.localeCompare(b.model);
265
+ });
266
+ return { rows };
267
+ }
268
+
269
+ async function exportSessionUsageCore(params = {}, deps = {}) {
270
+ const listSessionUsage = typeof deps.listSessionUsage === 'function'
271
+ ? deps.listSessionUsage
272
+ : (options) => listSessionUsageCore(options, deps);
273
+ const sessions = Array.isArray(params.sessions)
274
+ ? params.sessions
275
+ : await listSessionUsage({
276
+ source: params.source,
277
+ limit: params.limit,
278
+ forceRefresh: !!params.forceRefresh
279
+ });
280
+ const built = buildUsageExportRows(sessions, params);
281
+ if (built.error) {
282
+ return { error: built.error };
283
+ }
284
+ const format = normalizeUsageExportFormat(params.format);
285
+ const rows = built.rows;
286
+ const content = format === 'json'
287
+ ? JSON.stringify({ rows }, null, 2) + '\n'
288
+ : serializeUsageExportRowsToCsv(rows);
289
+ const extension = format === 'json' ? 'json' : 'csv';
290
+ return {
291
+ format,
292
+ mimeType: format === 'json' ? 'application/json' : 'text/csv',
293
+ fileName: `usage-export.${extension}`,
294
+ rows,
295
+ content
296
+ };
297
+ }
298
+
116
299
  module.exports = {
117
- listSessionUsageCore
300
+ listSessionUsageCore,
301
+ buildUsageExportRows,
302
+ exportSessionUsageCore,
303
+ serializeUsageExportRowsToCsv
118
304
  };
package/cli.js CHANGED
@@ -162,7 +162,8 @@ const {
162
162
  extractSessionDetailPreviewFromTailText,
163
163
  extractSessionDetailPreviewFromFileFast
164
164
  } = require('./lib/cli-sessions');
165
- const { listSessionUsageCore } = require('./cli/session-usage');
165
+ const { listSessionUsageCore, exportSessionUsageCore } = require('./cli/session-usage');
166
+ const { parseAnalyticsExportArgs } = require('./cli/analytics-export-args');
166
167
  const {
167
168
  readBundledWebUiCss,
168
169
  readBundledWebUiHtml,
@@ -2080,12 +2081,22 @@ function addProviderToConfig(params = {}) {
2080
2081
  const name = typeof params.name === 'string' ? params.name.trim() : '';
2081
2082
  const url = typeof params.url === 'string' ? params.url.trim() : '';
2082
2083
  const key = typeof params.key === 'string' ? params.key.trim() : '';
2084
+ const requireModel = !!params.requireModel;
2085
+ const fallbackModel = (() => {
2086
+ if (requireModel) return '';
2087
+ const list = readModels();
2088
+ return Array.isArray(list) && typeof list[0] === 'string' ? list[0].trim() : '';
2089
+ })();
2090
+ const model = typeof params.model === 'string' && params.model.trim()
2091
+ ? params.model.trim()
2092
+ : fallbackModel;
2083
2093
  const useTransform = !!params.useTransform;
2084
2094
  const allowManaged = !!params.allowManaged;
2085
2095
  const normalizedUrl = normalizeBaseUrl(url);
2086
2096
 
2087
2097
  if (!name) return { error: '名称不能为空' };
2088
2098
  if (!url) return { error: 'URL 不能为空' };
2099
+ if (!model) return { error: '模型名称不能为空' };
2089
2100
  if (!isValidProviderName(name)) {
2090
2101
  return { error: '名称仅支持字母/数字/._-' };
2091
2102
  }
@@ -2162,6 +2173,7 @@ function addProviderToConfig(params = {}) {
2162
2173
  `wire_api = "responses"`,
2163
2174
  `requires_openai_auth = ${requiresOpenaiAuth ? 'true' : 'false'}`,
2164
2175
  `preferred_auth_method = "${safeKey}"`,
2176
+ `models = [{ id = "${escapeTomlBasicString(model)}", name = "${escapeTomlBasicString(model)}" }]`,
2165
2177
  ...extraLines,
2166
2178
  `request_max_retries = 4`,
2167
2179
  `stream_max_retries = 10`,
@@ -2172,6 +2184,13 @@ function addProviderToConfig(params = {}) {
2172
2184
 
2173
2185
  try {
2174
2186
  writeConfig(newContent);
2187
+ const models = readModels();
2188
+ if (!models.includes(model)) {
2189
+ writeModels([...models, model]);
2190
+ }
2191
+ const currentModels = readCurrentModels();
2192
+ currentModels[name] = model;
2193
+ writeCurrentModels(currentModels);
2175
2194
  } catch (e) {
2176
2195
  return { error: `写入配置失败: ${e.message}` };
2177
2196
  }
@@ -5204,6 +5223,12 @@ async function listSessionUsage(params = {}) {
5204
5223
  });
5205
5224
  }
5206
5225
 
5226
+ async function exportSessionUsage(params = {}) {
5227
+ return exportSessionUsageCore(params, {
5228
+ listSessionUsage
5229
+ });
5230
+ }
5231
+
5207
5232
  function listSessionPaths(params = {}) {
5208
5233
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
5209
5234
  if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
@@ -9796,6 +9821,47 @@ async function cmdExportSession(args = []) {
9796
9821
  console.log();
9797
9822
  }
9798
9823
 
9824
+ function printAnalyticsUsage() {
9825
+ console.log('\n用法:');
9826
+ console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--source <codex|claude|gemini|codebuddy|all>] [--output <PATH|->] [-o <PATH|->]');
9827
+ console.log('');
9828
+ }
9829
+
9830
+ async function cmdAnalytics(args = []) {
9831
+ const subcommand = args[0];
9832
+ if (subcommand !== 'export') {
9833
+ printAnalyticsUsage();
9834
+ process.exit(subcommand ? 1 : 0);
9835
+ }
9836
+ const parsed = parseAnalyticsExportArgs(args.slice(1));
9837
+ if (parsed.options.help) {
9838
+ printAnalyticsUsage();
9839
+ process.exit(0);
9840
+ }
9841
+ if (parsed.error) {
9842
+ console.error('错误:', parsed.error);
9843
+ printAnalyticsUsage();
9844
+ process.exit(1);
9845
+ }
9846
+
9847
+ const result = await exportSessionUsage(parsed.options);
9848
+ if (result && result.error) {
9849
+ console.error('导出失败:', result.error);
9850
+ process.exit(1);
9851
+ }
9852
+ const output = parsed.options.output || (result && result.fileName) || `usage-export.${parsed.options.format}`;
9853
+ if (output === '-') {
9854
+ process.stdout.write(result && result.content ? result.content : '');
9855
+ return;
9856
+ }
9857
+ const outputPath = path.resolve(process.cwd(), output);
9858
+ ensureDir(path.dirname(outputPath));
9859
+ fs.writeFileSync(outputPath, result && result.content ? result.content : '', 'utf-8');
9860
+ console.log(`\n✓ Usage 已导出: ${outputPath}`);
9861
+ console.log(` 格式: ${result.format}; rows: ${Array.isArray(result.rows) ? result.rows.length : 0}`);
9862
+ console.log();
9863
+ }
9864
+
9799
9865
  function parseStartOptions(args = []) {
9800
9866
  const options = { host: '', noBrowser: false };
9801
9867
  if (!Array.isArray(args)) {
@@ -10818,7 +10884,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
10818
10884
  result = buildConfigTemplateDiff(params || {});
10819
10885
  break;
10820
10886
  case 'add-provider':
10821
- result = addProviderToConfig(params || {});
10887
+ result = addProviderToConfig({ ...(params || {}), requireModel: true });
10822
10888
  break;
10823
10889
  case 'update-provider':
10824
10890
  result = updateProviderInConfig(params || {});
@@ -11077,6 +11143,20 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11077
11143
  }
11078
11144
  }
11079
11145
  break;
11146
+ case 'export-sessions-usage':
11147
+ {
11148
+ const usageParams = isPlainObject(params) ? params : {};
11149
+ const source = typeof usageParams.source === 'string' ? usageParams.source.trim().toLowerCase() : '';
11150
+ if (source && source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'all') {
11151
+ result = { error: 'Invalid source. Must be codex, claude, gemini, codebuddy, or all' };
11152
+ } else {
11153
+ result = await exportSessionUsage({
11154
+ ...usageParams,
11155
+ source: source || 'all'
11156
+ });
11157
+ }
11158
+ }
11159
+ break;
11080
11160
  case 'list-session-paths':
11081
11161
  {
11082
11162
  const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
@@ -15960,6 +16040,7 @@ function printMainHelp() {
15960
16040
  console.log(' codexmate delete-model <模型> 删除模型');
15961
16041
  console.log(' codexmate workflow <list|get|validate|run|runs> MCP 工作流中心');
15962
16042
  console.log(' codexmate task <plan|run|runs|queue|retry|cancel|logs> 本地任务编排');
16043
+ console.log(' codexmate analytics export [--format csv|json] [--from YYYY-MM-DD] [--to YYYY-MM-DD] [--model <MODEL>] [--output <PATH|->] [-o <PATH|->] 导出 Usage 数据');
15963
16044
  console.log(' codexmate run [--host <HOST>] [--no-browser] 启动 Web 界面');
15964
16045
  console.log(' codexmate update [--check] 检查并快速更新工具');
15965
16046
  console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo');
@@ -16052,6 +16133,7 @@ async function main() {
16052
16133
  case 'proxy': await cmdProxy(args.slice(1)); break;
16053
16134
  case 'workflow': await cmdWorkflow(args.slice(1)); break;
16054
16135
  case 'task': await cmdTask(args.slice(1)); break;
16136
+ case 'analytics': await cmdAnalytics(args.slice(1)); break;
16055
16137
  case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
16056
16138
  case 'update': await cmdToolUpdate(args.slice(1)); break;
16057
16139
  case 'start':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -62,11 +62,13 @@ document.addEventListener('DOMContentLoaded', () => {
62
62
  messageType: '',
63
63
  showAddModal: false,
64
64
  showEditModal: false,
65
+ showAddProviderKey: false,
65
66
  showEditProviderKey: false,
66
67
  showModelModal: false,
67
68
  showModelListModal: false,
68
69
  showClaudeConfigModal: false,
69
70
  showEditConfigModal: false,
71
+ showAddClaudeConfigKey: false,
70
72
  showEditClaudeConfigKey: false,
71
73
  showOpenclawConfigModal: false,
72
74
  showConfigTemplateModal: false,
@@ -268,7 +270,7 @@ document.addEventListener('DOMContentLoaded', () => {
268
270
  installRegistryPreset: 'default',
269
271
  installRegistryCustom: '',
270
272
  installStatusTargets: null,
271
- newProvider: { name: '', url: '', key: '', useTransform: false, _suggestedModel: '' },
273
+ newProvider: { name: '', url: '', key: '', model: '', useTransform: false },
272
274
  resetConfigLoading: false,
273
275
  editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
274
276
  newModelName: '',
@@ -293,7 +295,8 @@ document.addEventListener('DOMContentLoaded', () => {
293
295
  currentOpenclawConfig: '',
294
296
  openclawConfigs: {
295
297
  '默认配置': {
296
- content: DEFAULT_OPENCLAW_TEMPLATE
298
+ content: DEFAULT_OPENCLAW_TEMPLATE,
299
+ isDefault: true
297
300
  }
298
301
  },
299
302
  openclawEditing: { name: '', content: '', lockName: false },
@@ -343,6 +346,11 @@ document.addEventListener('DOMContentLoaded', () => {
343
346
  overrideModels: true,
344
347
  showKey: false
345
348
  },
349
+ openclawAccordionStep: 1,
350
+ openclawValidation: {
351
+ providerName: { valid: true, message: '' },
352
+ modelId: { valid: true, message: '' }
353
+ },
346
354
  openclawAgentsList: [],
347
355
  openclawProviders: [],
348
356
  openclawMissingProviders: [],
@@ -567,7 +575,8 @@ document.addEventListener('DOMContentLoaded', () => {
567
575
  : { content: DEFAULT_OPENCLAW_TEMPLATE };
568
576
  const normalized = {
569
577
  '默认配置': {
570
- content: typeof defaultEntry.content === 'string' ? defaultEntry.content : DEFAULT_OPENCLAW_TEMPLATE
578
+ content: typeof defaultEntry.content === 'string' ? defaultEntry.content : DEFAULT_OPENCLAW_TEMPLATE,
579
+ isDefault: true
571
580
  }
572
581
  };
573
582
  for (const [name, value] of Object.entries(source)) {
@@ -43,93 +43,97 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) {
43
43
  };
44
44
  }
45
45
 
46
- function createTaskDraftChecklist(metrics) {
46
+ function translateTaskText(t, key, fallback, params = null) {
47
+ return typeof t === 'function' ? t(key, params) : fallback;
48
+ }
49
+
50
+ function createTaskDraftChecklist(metrics, t = null) {
47
51
  const workflowReady = metrics.engine !== 'workflow' || metrics.workflowCount > 0;
48
52
  const scopeReady = metrics.hasNotes || !metrics.allowWrite;
49
53
  const previewReady = metrics.hasPlan && metrics.planIssues.length === 0;
50
54
  return [
51
55
  {
52
56
  key: 'target',
53
- label: '目标',
57
+ label: translateTaskText(t, 'orchestration.readiness.target.label', '目标'),
54
58
  done: metrics.hasTarget,
55
- detail: metrics.hasTarget ? '已写目标' : '还没写目标'
59
+ detail: metrics.hasTarget ? translateTaskText(t, 'orchestration.readiness.target.done', '已写目标') : translateTaskText(t, 'orchestration.readiness.target.missing', '还没写目标')
56
60
  },
57
61
  {
58
62
  key: 'engine',
59
- label: metrics.engine === 'workflow' ? 'Workflow' : '执行策略',
63
+ label: metrics.engine === 'workflow' ? 'Workflow' : translateTaskText(t, 'orchestration.readiness.engine.label', '执行策略'),
60
64
  done: workflowReady,
61
65
  detail: metrics.engine === 'workflow'
62
- ? (metrics.workflowCount > 0 ? `已选 ${metrics.workflowCount} 个 Workflow` : '还没选 Workflow ID')
63
- : '使用 Codex 规划节点'
66
+ ? (metrics.workflowCount > 0 ? translateTaskText(t, 'orchestration.readiness.workflow.done', `已选 ${metrics.workflowCount} 个 Workflow`, { count: metrics.workflowCount }) : translateTaskText(t, 'orchestration.readiness.workflow.missing', '还没选 Workflow ID'))
67
+ : translateTaskText(t, 'orchestration.readiness.engine.codex', '使用 Codex 规划节点')
64
68
  },
65
69
  {
66
70
  key: 'scope',
67
- label: '边界',
71
+ label: translateTaskText(t, 'orchestration.readiness.scope.label', '边界'),
68
72
  done: scopeReady,
69
73
  detail: metrics.hasNotes
70
- ? '已补充说明'
71
- : (metrics.allowWrite ? '建议补说明后再写入' : '当前是只读,可直接试')
74
+ ? translateTaskText(t, 'orchestration.readiness.scope.done', '已补充说明')
75
+ : (metrics.allowWrite ? translateTaskText(t, 'orchestration.readiness.scope.writeHint', '建议补说明后再写入') : translateTaskText(t, 'orchestration.readiness.scope.readonlyHint', '当前是只读,可直接试'))
72
76
  },
73
77
  {
74
78
  key: 'preview',
75
- label: '预览',
79
+ label: translateTaskText(t, 'orchestration.readiness.preview.label', '预览'),
76
80
  done: previewReady,
77
81
  detail: !metrics.hasPlan
78
- ? '还没生成计划'
79
- : (metrics.planIssues.length > 0 ? `有 ${metrics.planIssues.length} 个阻塞项` : `计划可用,${metrics.planNodeCount} 个节点`)
82
+ ? translateTaskText(t, 'orchestration.readiness.preview.missing', '还没生成计划')
83
+ : (metrics.planIssues.length > 0 ? translateTaskText(t, 'orchestration.readiness.preview.blocked', `有 ${metrics.planIssues.length} 个阻塞项`, { count: metrics.planIssues.length }) : translateTaskText(t, 'orchestration.readiness.preview.ready', `计划可用,${metrics.planNodeCount} 个节点`, { count: metrics.planNodeCount }))
80
84
  }
81
85
  ];
82
86
  }
83
87
 
84
- function createTaskDraftReadiness(metrics) {
88
+ function createTaskDraftReadiness(metrics, t = null) {
85
89
  if (!metrics.hasTarget) {
86
90
  return {
87
91
  tone: 'neutral',
88
- title: '先写目标',
89
- summary: '先把想完成的结果写清楚,再让编排器拆节点。'
92
+ title: translateTaskText(t, 'orchestration.readiness.empty.title', '先写目标'),
93
+ summary: translateTaskText(t, 'orchestration.readiness.empty.summary', '先把想完成的结果写清楚,再让编排器拆节点。')
90
94
  };
91
95
  }
92
96
  if (metrics.engine === 'workflow' && metrics.workflowCount === 0) {
93
97
  return {
94
98
  tone: 'warn',
95
- title: '缺少 Workflow',
96
- summary: '你已经选了 Workflow 模式,但还没指定可复用流程。'
99
+ title: translateTaskText(t, 'orchestration.readiness.workflow.title', '缺少 Workflow'),
100
+ summary: translateTaskText(t, 'orchestration.readiness.workflow.summary', '你已经选了 Workflow 模式,但还没指定可复用流程。')
97
101
  };
98
102
  }
99
103
  if (!metrics.hasPlan) {
100
104
  return {
101
105
  tone: 'warn',
102
- title: '建议先预览',
103
- summary: '草稿已成形,先生成一次计划,确认节点和依赖再执行。'
106
+ title: translateTaskText(t, 'orchestration.readiness.preview.title', '建议先预览'),
107
+ summary: translateTaskText(t, 'orchestration.readiness.preview.summary', '草稿已成形,先生成一次计划,确认节点和依赖再执行。')
104
108
  };
105
109
  }
106
110
  if (metrics.planIssues.length > 0) {
107
111
  return {
108
112
  tone: 'error',
109
- title: '预览有阻塞',
110
- summary: `当前计划里还有 ${metrics.planIssues.length} 个阻塞项,先处理它们。`
113
+ title: translateTaskText(t, 'orchestration.readiness.blocked.title', '预览有阻塞'),
114
+ summary: translateTaskText(t, 'orchestration.readiness.blocked.summary', `当前计划里还有 ${metrics.planIssues.length} 个阻塞项,先处理它们。`, { count: metrics.planIssues.length })
111
115
  };
112
116
  }
113
117
  if (metrics.planWarnings.length > 0) {
114
118
  return {
115
119
  tone: 'warn',
116
- title: '可以执行,但有提醒',
117
- summary: `计划已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`
120
+ title: translateTaskText(t, 'orchestration.readiness.warn.title', '可以执行,但有提醒'),
121
+ summary: translateTaskText(t, 'orchestration.readiness.warn.summary', `计划已生成,但还有 ${metrics.planWarnings.length} 条提醒值得先看一眼。`, { count: metrics.planWarnings.length })
118
122
  };
119
123
  }
120
124
  if (metrics.dryRun) {
121
125
  return {
122
126
  tone: 'success',
123
- title: '适合先预演',
124
- summary: '现在可以安全地跑一次仅预演,先看结果再决定是否真实执行。'
127
+ title: translateTaskText(t, 'orchestration.readiness.dryRun.title', '适合先预演'),
128
+ summary: translateTaskText(t, 'orchestration.readiness.dryRun.summary', '现在可以安全地跑一次仅预演,先看结果再决定是否真实执行。')
125
129
  };
126
130
  }
127
131
  return {
128
132
  tone: 'success',
129
- title: '可以执行',
133
+ title: translateTaskText(t, 'orchestration.readiness.ready.title', '可以执行'),
130
134
  summary: metrics.followUpCount > 0
131
- ? `主目标和收尾动作都已具备,可以直接执行或入队。`
132
- : '主目标已经够清楚了,可以直接执行或入队。'
135
+ ? translateTaskText(t, 'orchestration.readiness.ready.withFollowUps', `主目标和收尾动作都已具备,可以直接执行或入队。`)
136
+ : translateTaskText(t, 'orchestration.readiness.ready.summary', '主目标已经够清楚了,可以直接执行或入队。')
133
137
  };
134
138
  }
135
139
 
@@ -144,6 +148,7 @@ export function createMainTabsComputed() {
144
148
  if (this.mainTab === 'market') return this.t('kicker.market');
145
149
  if (this.mainTab === 'plugins') return this.t('kicker.plugins');
146
150
  if (this.mainTab === 'docs') return this.t('kicker.docs');
151
+ if (this.mainTab === 'trash') return this.t('kicker.trash');
147
152
  return this.t('kicker.settings');
148
153
  },
149
154
  mainTabTitle() {
@@ -155,6 +160,7 @@ export function createMainTabsComputed() {
155
160
  if (this.mainTab === 'market') return this.t('title.market');
156
161
  if (this.mainTab === 'plugins') return this.t('title.plugins');
157
162
  if (this.mainTab === 'docs') return this.t('title.docs');
163
+ if (this.mainTab === 'trash') return this.t('settings.trash.title');
158
164
  return this.t('title.settings');
159
165
  },
160
166
  mainTabSubtitle() {
@@ -166,6 +172,7 @@ export function createMainTabsComputed() {
166
172
  if (this.mainTab === 'market') return this.t('subtitle.market');
167
173
  if (this.mainTab === 'plugins') return this.t('subtitle.plugins');
168
174
  if (this.mainTab === 'docs') return this.t('subtitle.docs');
175
+ if (this.mainTab === 'trash') return this.t('settings.trash.meta');
169
176
  return this.t('subtitle.settings');
170
177
  },
171
178
  taskOrchestrationSelectedRun() {
@@ -196,10 +203,10 @@ export function createMainTabsComputed() {
196
203
  return readTaskOrchestrationDraftMetrics(this.taskOrchestration);
197
204
  },
198
205
  taskOrchestrationDraftChecklist() {
199
- return createTaskDraftChecklist(this.taskOrchestrationDraftMetrics);
206
+ return createTaskDraftChecklist(this.taskOrchestrationDraftMetrics, this.t && this.t.bind(this));
200
207
  },
201
208
  taskOrchestrationDraftReadiness() {
202
- return createTaskDraftReadiness(this.taskOrchestrationDraftMetrics);
209
+ return createTaskDraftReadiness(this.taskOrchestrationDraftMetrics, this.t && this.t.bind(this));
203
210
  }
204
211
  };
205
212
  }