@alipay/alipay-aipay 1.6.7-beta2 → 1.6.7

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.
@@ -4,12 +4,12 @@ import https from 'node:https';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import process from 'node:process';
7
- import { execFileSync, spawn } from 'node:child_process';
7
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
8
8
  import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
11
11
  const disableValues = new Set(['0', 'false', 'off', 'no']);
12
- const canonicalProductTypes = new Set(['AIPAY', 'WEBPAY', 'APPPAY', 'UNKNOWN']);
12
+ const canonicalProductTypes = new Set(['AIPAY', 'WEBPAY', 'APPPAY']);
13
13
  const allowedRootIntents = new Set(['integration_only', 'onboarding_only', 'full_process']);
14
14
  const syncLogBizType = 'yuyanmonitorl';
15
15
  const syncLogServerURL = 'https://collect.alipay.com/yuyan/';
@@ -17,7 +17,7 @@ const syncLogMonitorVersion = 'alipay-aipay-skill:sync-log';
17
17
  const syncLogCode = '1005';
18
18
  const syncLogMsg = 'alipay_aipay_skill_event';
19
19
  const syncLogYuyanID = '180020010001290755';
20
- const uploadTimeoutMs = 2500;
20
+ const uploadTimeoutMs = 3000;
21
21
  const permissionedForegroundFlushLimit = 5;
22
22
  const localWriteMaxAttempts = 2;
23
23
  const runStateLockWaitMs = 5000;
@@ -51,24 +51,13 @@ const permissionedFlushNetworkErrors = new Set([
51
51
  'ENETUNREACH',
52
52
  'NETWORK_UNREACHABLE'
53
53
  ]);
54
- const productByVariant = { AIPAY: 'AIPAY', WEBPAY: 'WEBPAY', APPPAY: 'APPPAY' };
55
- const productByName = {
56
- 'AI 按量付费': 'AIPAY',
57
- 'AI 网页应用收款': 'WEBPAY',
58
- 'AI 移动应用收款': 'APPPAY',
59
- 按量付费: 'AIPAY',
60
- 网站支付: 'WEBPAY',
61
- 'APP 支付': 'APPPAY'
62
- };
63
54
  const productAliases = {
64
55
  aipay: 'AIPAY',
65
56
  webpay: 'WEBPAY',
66
57
  apppay: 'APPPAY',
67
- unknown: 'UNKNOWN',
68
58
  AIPAY: 'AIPAY',
69
59
  WEBPAY: 'WEBPAY',
70
- APPPAY: 'APPPAY',
71
- UNKNOWN: 'UNKNOWN'
60
+ APPPAY: 'APPPAY'
72
61
  };
73
62
 
74
63
  export const approvedStages = Object.freeze([
@@ -142,17 +131,11 @@ const countedStageRoutes = Object.freeze({
142
131
  });
143
132
 
144
133
  const summaryStages = new Set(['integration_summary', 'onboarding_summary', 'all_summary']);
145
- const terminalMessageIds = new Set([
146
- 'product.unsupported',
147
- 'onboarding.windows.redirect',
148
- 'onboarding.enterprise.unsupported'
149
- ]);
150
-
151
134
  function flowForStage(stage) {
152
135
  if (stage.startsWith('integration.')) return 'integration';
153
136
  if (stage.startsWith('onboarding.')) return 'onboarding';
154
137
  if (summaryStages.has(stage)) return 'common';
155
- return '';
138
+ return 'common';
156
139
  }
157
140
 
158
141
  const stageRules = new Map(approvedStages.map((stage) => [stage, {
@@ -166,10 +149,9 @@ function routeStages(rootIntent, productType) {
166
149
  }
167
150
 
168
151
  function completedStageList(state) {
169
- const progress = state?.stage_progress;
170
- if (!progress || typeof progress !== 'object' || Array.isArray(progress)) return [];
171
- const stages = Array.isArray(progress.completed_stages) ? progress.completed_stages : [];
172
- return stages.filter((stage) => approvedStages.includes(stage));
152
+ const events = state?.stage_events;
153
+ if (!events || typeof events !== 'object' || Array.isArray(events)) return [];
154
+ return Object.keys(events).filter((stage) => approvedStages.includes(stage));
173
155
  }
174
156
 
175
157
  function stageAlreadyRecorded(state, stage) {
@@ -201,7 +183,9 @@ function messageStageFor(messageId, context = {}, env = process.env) {
201
183
  case 'integration.checklist.result':
202
184
  return 'integration_summary';
203
185
  case 'process.partial_result':
204
- return context.rootIntent === 'full_process' || context.root_intent === 'full_process'
186
+ return context.rootIntent === 'full_process'
187
+ || context.root_intent === 'full_process'
188
+ || currentRunContext(env)?.root_intent === 'full_process'
205
189
  ? 'all_summary'
206
190
  : 'onboarding_summary';
207
191
  default:
@@ -218,60 +202,6 @@ function shouldCloseRunAfterStage(event, state = {}) {
218
202
  return false;
219
203
  }
220
204
 
221
- function closeRunLifecycleAfterStage(event, env = process.env) {
222
- const transaction = withRunStateLock(env, () => {
223
- const existing = readRunState(env);
224
- if (!existing?.run_id || existing.run_id !== event.run_id) return false;
225
- if (!shouldCloseRunAfterStage(event, existing)) return true;
226
- const nowMs = Number.isFinite(Number(event.timestamp)) ? Number(event.timestamp) : Date.now();
227
- return persistRunState({
228
- ...existing,
229
- terminal: true,
230
- terminal_at: nowMs,
231
- updated_at: nowMs
232
- }, env);
233
- });
234
- if (!transaction.acquired) {
235
- recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to lock local telemetry run state', env);
236
- return false;
237
- }
238
- return transaction.value;
239
- }
240
-
241
- function updateExistingRunStateFromMessage(messageId, context = {}, env = process.env) {
242
- const transaction = withRunStateLock(env, () => {
243
- const existing = readRunState(env);
244
- if (!existing?.run_id) return false;
245
- if (!runStateCompatibleWithEventContext(existing, context)) return true;
246
- const rootIntent = context.rootIntent || context.root_intent || existing.root_intent || '';
247
- if (messageId === 'integration.checklist.result') {
248
- if (rootIntent !== 'integration_only') return true;
249
- } else if (messageId === 'process.partial_result') {
250
- if (!['integration_only', 'onboarding_only', 'full_process'].includes(rootIntent)) return true;
251
- } else if (!terminalMessageIds.has(messageId)) {
252
- return false;
253
- }
254
- const nowMs = Date.now();
255
- const next = {
256
- ...existing,
257
- schema_ver: 1,
258
- run_id: existing.run_id,
259
- root_intent: existing.root_intent || rootIntent,
260
- product_type: existing.product_type || productTypeFrom({ context, env }),
261
- created_at: existing.created_at || nowMs,
262
- updated_at: nowMs,
263
- terminal: true,
264
- terminal_at: nowMs
265
- };
266
- return persistRunState(next, env);
267
- });
268
- if (!transaction.acquired) {
269
- recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to lock local telemetry run state', env);
270
- return false;
271
- }
272
- return transaction.value;
273
- }
274
-
275
205
  const outboxRecordFields = new Set(['schema_ver', 'created_at', 'event']);
276
206
  const metricEventFields = new Set([
277
207
  'schema_ver',
@@ -288,6 +218,7 @@ const metricEventFields = new Set([
288
218
  ]);
289
219
  const stageExtInfoFields = new Set([
290
220
  'user_input_after_clarify_intent',
221
+ 'history_count',
291
222
  'root_intent',
292
223
  'progress_pct'
293
224
  ]);
@@ -354,7 +285,10 @@ function isMaintenanceProcess(env = process.env, argv = process.argv) {
354
285
  }
355
286
 
356
287
  function stableStateSource(env = process.env) {
357
- return env.ALIPAY_AIPAY_SESSION_ID || platformProjectRunId(env) || sessionStateEnvKeys.map((key) => env[key]).find(Boolean) || '';
288
+ return env.ALIPAY_AIPAY_SESSION_ID
289
+ || sessionStateEnvKeys.map((key) => safeRunId(env[key])).find(Boolean)
290
+ || platformProjectId(env)
291
+ || '';
358
292
  }
359
293
 
360
294
  function stateKeyFromSource(source) {
@@ -468,18 +402,52 @@ function withRunStateLock(env = process.env, operation) {
468
402
  }
469
403
  }
470
404
 
405
+ function isValidRunState(parsed) {
406
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
407
+ if (parsed.schema_ver !== 2
408
+ || !safeRunId(parsed.run_id)
409
+ || !allowedRootIntents.has(parsed.root_intent)
410
+ || !canonicalProductTypes.has(parsed.product_type)
411
+ || !['ACTIVE', 'CLOSED'].includes(parsed.lifecycle)) return false;
412
+ const createdAt = Number(parsed.created_at);
413
+ const updatedAt = Number(parsed.updated_at);
414
+ if (!Number.isSafeInteger(createdAt) || createdAt <= 0
415
+ || !Number.isSafeInteger(updatedAt) || updatedAt <= 0) return false;
416
+
417
+ // Telemetry-off runs intentionally retain only the context needed by business CLI attribution.
418
+ if (parsed.telemetry_disabled === true) {
419
+ return !Object.prototype.hasOwnProperty.call(parsed, 'counter_count')
420
+ && !Object.prototype.hasOwnProperty.call(parsed, 'input_hashes')
421
+ && !Object.prototype.hasOwnProperty.call(parsed, 'stage_events')
422
+ && parsed.terminal_at === null;
423
+ }
424
+
425
+ if (parsed.clarify_intent_recorded !== true
426
+ || !Number.isSafeInteger(parsed.counter_count)
427
+ || parsed.counter_count < 0
428
+ || !Array.isArray(parsed.input_hashes)
429
+ || new Set(parsed.input_hashes).size !== parsed.input_hashes.length
430
+ || parsed.input_hashes.some((hash) => typeof hash !== 'string' || !/^[a-f0-9]{32}$/u.test(hash))
431
+ || !parsed.stage_events || typeof parsed.stage_events !== 'object' || Array.isArray(parsed.stage_events)) return false;
432
+
433
+ const stageNames = Object.keys(parsed.stage_events);
434
+ if (!stageNames.includes('clarify_intent') || stageNames.some((stage) => !approvedStages.includes(stage))) return false;
435
+ for (const record of Object.values(parsed.stage_events)) {
436
+ if (!record || typeof record !== 'object' || Array.isArray(record)
437
+ || !safeText(record.event_id) || record.event_id.length > 128
438
+ || !Number.isSafeInteger(record.generated_at) || record.generated_at <= 0) return false;
439
+ }
440
+ if (parsed.lifecycle === 'ACTIVE' && parsed.terminal_at !== null) return false;
441
+ if (parsed.lifecycle === 'CLOSED'
442
+ && (!Number.isSafeInteger(parsed.terminal_at) || parsed.terminal_at <= 0)) return false;
443
+ return true;
444
+ }
445
+
471
446
  function readRunState(env = process.env) {
472
447
  try {
473
448
  const statePath = runStatePath(env);
474
449
  const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
475
- if (!parsed || typeof parsed !== 'object') return null;
476
- if (parsed.schema_ver !== 1) return null;
477
- if (!safeRunId(parsed.run_id)) return null;
478
- const createdAt = Number(parsed.created_at);
479
- const updatedAt = Number(parsed.updated_at);
480
- if (!Number.isFinite(createdAt) || createdAt <= 0) return null;
481
- if (!Number.isFinite(updatedAt) || updatedAt <= 0) return null;
482
- return parsed;
450
+ return isValidRunState(parsed) ? parsed : null;
483
451
  } catch {
484
452
  return null;
485
453
  }
@@ -492,7 +460,7 @@ export function currentRunContext(env = process.env) {
492
460
  run_id: state.run_id,
493
461
  root_intent: state.root_intent,
494
462
  product_type: state.product_type,
495
- terminal: state.terminal === true
463
+ terminal: state.lifecycle === 'CLOSED'
496
464
  });
497
465
  }
498
466
 
@@ -501,6 +469,7 @@ function canReadImplicitRunState(env = process.env) {
501
469
  }
502
470
 
503
471
  function recordLocalTelemetryFailure(errorCode, message, env = process.env) {
472
+ if (!telemetryEnabled(env)) return;
504
473
  const safeCode = safeDimension(errorCode, 'LOCAL_TELEMETRY_FAILED', 80);
505
474
  const safeMessage = safeDimension(message || safeCode, safeCode, 240);
506
475
  writeUploadStatus({
@@ -530,6 +499,7 @@ function outboxFilename(event) {
530
499
  }
531
500
 
532
501
  function writeOutboxRecord(event, env = process.env) {
502
+ if (!telemetryEnabled(env)) return '';
533
503
  try {
534
504
  const normalizedEvent = normalizeMetricEvent(event);
535
505
  if (!normalizedEvent) return '';
@@ -550,6 +520,7 @@ function writeOutboxRecord(event, env = process.env) {
550
520
  }
551
521
 
552
522
  export function enqueueMetricEvent(event, env = process.env) {
523
+ if (!telemetryEnabled(env)) return false;
553
524
  return Boolean(writeOutboxRecord(event, env));
554
525
  }
555
526
 
@@ -558,50 +529,20 @@ function safeRunId(value) {
558
529
  return /[\u0000-\u001f\u007f,^=]/u.test(value) ? '' : value;
559
530
  }
560
531
 
561
- function platformProjectRunId(env = process.env) {
532
+ function platformProjectId(env = process.env) {
562
533
  for (const key of platformProjectIdEnvKeys) {
563
534
  const value = safeRunId(env[key]);
564
535
  if (value) return value;
565
536
  }
566
537
  if (env.MIAODA) {
567
- return safeRunId(env.VITE_APP_ID) || safeRunId(env.PLATFORM_ID) || '';
538
+ return safeRunId(env.VITE_APP_ID) || '';
568
539
  }
569
540
  return '';
570
541
  }
571
542
 
572
- function explicitRunIdFrom(context, env) {
573
- return safeRunId(context.runId || context.run_id || env.ALIPAY_AIPAY_RUN_ID || env.ALIPAY_AIPAY_FLOW_RUN_ID);
574
- }
575
-
576
- function runIdFrom(context, env) {
577
- const state = canReadImplicitRunState(env) ? readRunState(env) : null;
578
- return safeRunId(platformProjectRunId(env) || explicitRunIdFrom(context, env) || state?.run_id);
579
- }
580
-
581
- function rootIntentFrom(context, env) {
582
- const state = canReadImplicitRunState(env) ? readRunState(env) : null;
583
- const value = context.rootIntent || context.root_intent || env.ALIPAY_AIPAY_ROOT_INTENT || state?.root_intent;
584
- return allowedRootIntents.has(value) ? value : '';
585
- }
586
-
587
543
  function canonicalProductType(value) {
588
- return typeof value === 'string' && productAliases[value] ? productAliases[value] : '';
589
- }
590
-
591
- function productTypeFrom({ ruleProductType, variantName, variables = {}, context = {}, env = process.env }) {
592
- const explicit = context.productType || context.product_type || ruleProductType;
593
- const explicitCanonical = canonicalProductType(explicit);
594
- if (explicitCanonical) return explicitCanonical;
595
- const variableCanonical = canonicalProductType(variables.productType);
596
- if (variableCanonical) return variableCanonical;
597
- if (productByVariant[variantName]) return productByVariant[variantName];
598
- if (productByName[variables.productName]) return productByName[variables.productName];
599
- if (productByName[context.productName]) return productByName[context.productName];
600
- const stateCanonical = canReadImplicitRunState(env) ? canonicalProductType(readRunState(env)?.product_type) : '';
601
- if (stateCanonical) return stateCanonical;
602
- const envCanonical = canonicalProductType(env.ALIPAY_AIPAY_PRODUCT_TYPE);
603
- if (envCanonical) return envCanonical;
604
- return 'UNKNOWN';
544
+ const canonical = typeof value === 'string' ? productAliases[value] : '';
545
+ return canonicalProductTypes.has(canonical) ? canonical : '';
605
546
  }
606
547
 
607
548
  function parseNonNegativeInteger(value) {
@@ -618,15 +559,6 @@ function sleepSync(ms) {
618
559
  } catch {}
619
560
  }
620
561
 
621
- function parseBooleanFlag(value) {
622
- if (typeof value === 'boolean') return value;
623
- if (typeof value !== 'string') return null;
624
- const normalized = normalizeSwitch(value);
625
- if (normalized === 'true' || normalized === '1') return true;
626
- if (normalized === 'false' || normalized === '0') return false;
627
- return null;
628
- }
629
-
630
562
  function normalizeUserInputId(value) {
631
563
  if (typeof value !== 'string') return '';
632
564
  const text = value.trim();
@@ -645,154 +577,70 @@ function normalizeUserInputHashes(value) {
645
577
  return [...new Set(value.filter((entry) => typeof entry === 'string' && /^[a-f0-9]{32}$/u.test(entry)))];
646
578
  }
647
579
 
648
- function normalizeUserInputCounts(value) {
649
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
650
- return { user_input_after_clarify_intent: 0, input_hashes: [] };
651
- }
652
- const count = parseNonNegativeInteger(
653
- value.user_input_after_clarify_intent ?? value.userInputAfterClarifyIntent
654
- );
655
- if (count === null) {
656
- return { user_input_after_clarify_intent: 0, input_hashes: [] };
657
- }
658
- return {
659
- user_input_after_clarify_intent: count,
660
- input_hashes: normalizeUserInputHashes(value.input_hashes),
661
- updated_at: parseNonNegativeInteger(value.updated_at) || undefined
662
- };
580
+ function parseHistoryCount(value, { strict = false } = {}) {
581
+ if (value === null || value === 'null' || value === undefined) return null;
582
+ const parsed = parseNonNegativeInteger(value);
583
+ if (parsed !== null) return parsed;
584
+ if (strict) throw new Error('--history-count 只支持非负整数或 null');
585
+ return undefined;
663
586
  }
664
587
 
665
- function validateStageCountPayload(payload = {}, env = process.env, { strict = false } = {}) {
666
- const fail = (message) => {
667
- if (strict) throw new Error(message);
668
- return null;
669
- };
588
+ function stageExtInfoFromPayload(payload = {}) {
589
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
670
590
  const userInputAfterClarifyIntent = parseNonNegativeInteger(
671
591
  payload.userInputAfterClarifyIntent ?? payload.user_input_after_clarify_intent
672
592
  );
673
- const rootIntent = rootIntentFrom(payload, env);
674
- if (userInputAfterClarifyIntent === null) {
675
- return fail('stage 用户输入计数必须是合法非负整数');
676
- }
677
- if (!rootIntent) return fail('stage 用户输入计数缺少合法 root_intent');
678
- return { userInputAfterClarifyIntent, rootIntent };
679
- }
680
-
681
- function initialUserInputCounts(nowMs = Date.now()) {
682
- return {
683
- user_input_after_clarify_intent: 0,
684
- input_hashes: [],
685
- updated_at: nowMs
686
- };
687
- }
688
-
689
- function addUserInputCount(counts, inputId, runId, nowMs = Date.now()) {
690
- const inputHash = userInputHash(inputId, runId);
691
- if (!inputHash || counts.input_hashes.includes(inputHash)) return counts;
692
- return {
693
- ...counts,
694
- user_input_after_clarify_intent: counts.user_input_after_clarify_intent + 1,
695
- input_hashes: [...counts.input_hashes, inputHash],
696
- updated_at: nowMs
697
- };
698
- }
699
-
700
- function publicUserInputCountState(counts) {
701
- if (!counts) return null;
702
- const normalized = normalizeUserInputCounts(counts);
703
- return {
704
- user_input_after_clarify_intent: normalized.user_input_after_clarify_intent
705
- };
706
- }
707
-
708
- function normalizeStageProgress(progress) {
709
- if (!progress || typeof progress !== 'object' || Array.isArray(progress)) {
710
- return { completed_stages: [], updated_at: 0 };
711
- }
712
- const completedStages = Array.isArray(progress.completed_stages)
713
- ? progress.completed_stages.filter((stage) => approvedStages.includes(stage))
714
- : [];
715
- const seen = new Set();
716
- return {
717
- completed_stages: completedStages.filter((stage) => {
718
- if (seen.has(stage)) return false;
719
- seen.add(stage);
720
- return true;
721
- }),
722
- updated_at: Number.isFinite(Number(progress.updated_at)) ? Number(progress.updated_at) : 0
723
- };
724
- }
725
-
726
- function stageExtInfoFromPayload(payload = {}, env = process.env) {
727
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
728
- const counts = validateStageCountPayload(payload, env);
729
- if (!counts) return '';
593
+ if (userInputAfterClarifyIntent === null) return '';
594
+ const historyCount = parseHistoryCount(payload.historyCount ?? payload.history_count);
595
+ if (historyCount === undefined) return '';
596
+ const rootIntent = payload.rootIntent ?? payload.root_intent;
597
+ if (!allowedRootIntents.has(rootIntent)) return '';
730
598
  const progressPct = Number(payload.progressPct ?? payload.progress_pct);
731
599
  if (!Number.isSafeInteger(progressPct) || progressPct < 0 || progressPct > 100) return '';
732
600
  return JSON.stringify({
733
- user_input_after_clarify_intent: counts.userInputAfterClarifyIntent,
734
- root_intent: counts.rootIntent,
601
+ user_input_after_clarify_intent: userInputAfterClarifyIntent,
602
+ history_count: historyCount,
603
+ root_intent: rootIntent,
735
604
  progress_pct: progressPct
736
605
  });
737
606
  }
738
607
 
739
- export function stageMetricEvent(stage, context = {}, nowMs = Date.now(), env = process.env) {
740
- const rule = stageRules.get(stage);
741
- if (!rule) return null;
742
- const state = canReadImplicitRunState(env) ? readRunState(env) : null;
743
- const explicitRunId = platformProjectRunId(env) || explicitRunIdFrom(context, env);
744
- if (state && (!explicitRunId || explicitRunId === state.run_id) && !runStateCompatibleWithEventContext(state, context)) {
745
- return null;
746
- }
747
- const runId = runIdFrom(context, env);
748
- if (!runId) return null;
749
- if (state && state.run_id === runId && stageAlreadyRecorded(state, rule.stage)) return null;
750
- const event = {
608
+ function legalStageForState(state, stage) {
609
+ if (!state || !stageRules.has(stage)) return false;
610
+ if (stage === 'clarify_intent') return true;
611
+ if (stage === 'integration_summary') return ['integration_only', 'full_process'].includes(state.root_intent);
612
+ if (stage === 'onboarding_summary') return state.root_intent === 'onboarding_only';
613
+ if (stage === 'all_summary') return state.root_intent === 'full_process';
614
+ return routeStages(state.root_intent, state.product_type).includes(stage);
615
+ }
616
+
617
+ function eventFromState(stage, state, historyCount, nowMs = Date.now(), eventId = crypto.randomUUID()) {
618
+ if (!legalStageForState(state, stage)) return null;
619
+ const completedStages = completedStageList(state);
620
+ const progressPct = stage === 'clarify_intent'
621
+ ? 0
622
+ : progressPctForStage(state.root_intent, state.product_type, stage, completedStages);
623
+ if (progressPct < 0) return null;
624
+ const extInfo = stageExtInfoFromPayload({
625
+ userInputAfterClarifyIntent: state.counter_count,
626
+ historyCount,
627
+ rootIntent: state.root_intent,
628
+ progressPct
629
+ });
630
+ if (!extInfo) return null;
631
+ return {
751
632
  schema_ver: 2,
752
- event_id: crypto.randomUUID(),
633
+ event_id: eventId,
753
634
  skill: 'alipay-aipay',
754
635
  skill_version: skillVersionFromDisk(),
755
- run_id: runId,
756
- stage: rule.stage,
636
+ run_id: state.run_id,
637
+ stage,
757
638
  timestamp: nowMs,
758
- flow: rule.flow,
759
- product_type: productTypeFrom({ ruleProductType: context.productType || context.product_type, context, env })
639
+ flow: flowForStage(stage),
640
+ product_type: state.product_type,
641
+ root_intent: state.root_intent,
642
+ ext_info: extInfo
760
643
  };
761
- if (!canonicalProductTypes.has(event.product_type)) event.product_type = 'UNKNOWN';
762
- const rootIntent = rootIntentFrom(context, env);
763
- if (rootIntent) event.root_intent = rootIntent;
764
- const completedStages = completedStageList(state);
765
- const progressPct = event.stage === 'clarify_intent'
766
- ? 0
767
- : progressPctForStage(rootIntent || state?.root_intent || '', event.product_type, event.stage, completedStages);
768
- if (progressPct >= 0) {
769
- const extInfo = stageExtInfoFromPayload({
770
- userInputAfterClarifyIntent: state?.user_input_counts?.user_input_after_clarify_intent ?? 0,
771
- rootIntent: rootIntent || state?.root_intent || '',
772
- progressPct
773
- }, env);
774
- if (!extInfo) return null;
775
- event.ext_info = extInfo;
776
- } else {
777
- return null;
778
- }
779
- return event;
780
- }
781
-
782
- export function messageMetricEvent(messageId, variantName, variables = {}, context = {}, nowMs = Date.now(), env = process.env) {
783
- const stage = messageStageFor(messageId, context, env);
784
- if (!stage) return null;
785
- const stageEvent = stageMetricEvent(stage, {
786
- ...context,
787
- productType: productTypeFrom({ variantName, variables, context, env })
788
- }, nowMs, env);
789
- return stageEvent;
790
- }
791
-
792
- function scheduleQueuedSyncLogUpload(event, queuedFile, env = process.env) {
793
- if (!queuedFile || !telemetryUploadEnabled(env)) return false;
794
- spawnOutboxFlush(env);
795
- return true;
796
644
  }
797
645
 
798
646
  function writeMetricEventMarker(event, stream) {
@@ -801,126 +649,36 @@ function writeMetricEventMarker(event, stream) {
801
649
  }
802
650
  }
803
651
 
804
- export function emitMetricEvent(event, { env = process.env, stream = process.stderr, upload = true } = {}) {
805
- if (!telemetryEnabled(env)) return false;
806
- const normalizedEvent = normalizeMetricEvent(event);
807
- if (!normalizedEvent) return false;
808
- writeMetricEventMarker(normalizedEvent, stream);
809
- if (!upload) {
810
- updateExistingRunStateFromStageEvent(normalizedEvent, env);
811
- closeRunLifecycleAfterStage(normalizedEvent, env);
812
- return true;
813
- }
814
- if (telemetryCaptureOnly(env)) {
815
- if (writeOutboxRecord(normalizedEvent, env)) {
816
- updateExistingRunStateFromStageEvent(normalizedEvent, env);
817
- closeRunLifecycleAfterStage(normalizedEvent, env);
818
- return true;
819
- }
820
- recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'outbox write failed', env);
821
- return false;
822
- }
823
- if (!telemetryUploadEnabled(env)) {
824
- updateExistingRunStateFromStageEvent(normalizedEvent, env);
825
- closeRunLifecycleAfterStage(normalizedEvent, env);
826
- return true;
827
- }
828
- const queuedFile = writeOutboxRecord(normalizedEvent, env);
829
- if (!queuedFile) {
830
- recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'outbox write failed', env);
831
- return false;
832
- }
833
- updateExistingRunStateFromStageEvent(normalizedEvent, env);
834
- scheduleQueuedSyncLogUpload(normalizedEvent, queuedFile, env);
835
- closeRunLifecycleAfterStage(normalizedEvent, env);
836
- return true;
837
- }
838
-
839
- export function emitStageMetricAsync(stage, context = {}, env = process.env) {
652
+ export function emitStageMetric(stage, context = {}, env = process.env) {
840
653
  try {
841
654
  if (!telemetryEnabled(env) || !stageRules.has(stage)) return false;
842
- const event = stageMetricEvent(stage, context, Date.now(), env);
843
- if (!event) return false;
844
- if (normalizeSwitch(env.ALIPAY_AIPAY_TEST_MODE) === '1') {
845
- return emitMetricEvent(event, { env });
846
- }
847
- const args = [scriptPath(), 'stage', event.stage, '--timestamp-ms', String(event.timestamp)];
848
- const options = [
849
- ['run-id', event.run_id],
850
- ['root-intent', event.root_intent],
851
- ['product-type', event.product_type]
852
- ];
853
- for (const [name, value] of options) {
854
- if (typeof value === 'string' && value) args.push(`--${name}`, value);
855
- }
856
- const child = spawn(process.execPath, args, {
857
- detached: true,
858
- stdio: 'ignore',
859
- env
655
+ const activeRun = currentRunContext(env);
656
+ const runId = safeRunId(context.runId || context.run_id || env.ALIPAY_AIPAY_TELEMETRY_RUN_ID || activeRun?.run_id);
657
+ const historyCount = context.historyCount ?? context.history_count ?? env.ALIPAY_AIPAY_TELEMETRY_HISTORY_COUNT ?? 'null';
658
+ const child = spawnSync(process.execPath, [
659
+ scriptPath(), 'stage', stage,
660
+ '--run-id', runId,
661
+ '--history-count', String(historyCount)
662
+ ], {
663
+ stdio: normalizeSwitch(env.ALIPAY_AIPAY_TEST_MODE) === '1' ? 'inherit' : 'ignore',
664
+ env,
665
+ timeout: uploadTimeoutMs + 2000
860
666
  });
861
- child.on('error', (error) => {
862
- recordLocalTelemetryFailure(
863
- safeDimension(error?.code || error?.name || 'TELEMETRY_STAGE_SPAWN_FAILED', 'TELEMETRY_STAGE_SPAWN_FAILED', 80),
864
- safeDimension(error?.message || 'failed to start telemetry stage process', 'failed to start telemetry stage process', 240),
865
- env
866
- );
867
- });
868
- child.unref();
869
- return true;
667
+ return !child.error && child.status === 0;
870
668
  } catch {
871
669
  return false;
872
670
  }
873
671
  }
874
672
 
875
- export function emitMessageMetric(messageId, variantName, variables = {}, context = {}) {
673
+ export function emitMessageMetric(messageId, variantName, variables = {}, context = {}, env = process.env) {
876
674
  try {
877
- const event = messageMetricEvent(messageId, variantName, variables, context);
878
- const emitted = event ? emitMetricEvent(event, { stream: null }) : false;
879
- const lifecycleUpdated = updateExistingRunStateFromMessage(messageId, context);
880
- return emitted || lifecycleUpdated;
675
+ const stage = messageStageFor(messageId, context, env);
676
+ return stage ? emitStageMetric(stage, context, env) : false;
881
677
  } catch {
882
678
  return false;
883
679
  }
884
680
  }
885
681
 
886
- export async function startTelemetryRun(rootIntent, productType, inputId = '', {
887
- runId = '',
888
- env = process.env,
889
- nowMs = Date.now()
890
- } = {}) {
891
- if (typeof rootIntent !== 'string' || rootIntent.length === 0) return null;
892
- if (!allowedRootIntents.has(rootIntent)) {
893
- throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
894
- }
895
- const canonicalProduct = canonicalProductType(productType);
896
- if (!canonicalProduct || canonicalProduct === 'UNKNOWN') return null;
897
- const transaction = withRunStateLock(env, () => {
898
- const run = createOrReuseRunState({
899
- 'root-intent': rootIntent,
900
- 'product-type': canonicalProduct,
901
- 'run-id': runId
902
- }, env, nowMs);
903
- return {
904
- run,
905
- persisted: Boolean(run?.state && persistRunState(run.state, env))
906
- };
907
- });
908
- if (!transaction.acquired) {
909
- recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to lock local telemetry run state', env);
910
- return null;
911
- }
912
- const { run, persisted } = transaction.value || {};
913
- if (run?.state && persisted && run.reused !== true) {
914
- const event = stageMetricEvent('clarify_intent', {
915
- runId: run.state.run_id,
916
- rootIntent: run.state.root_intent,
917
- productType: run.state.product_type
918
- }, nowMs, env);
919
- emitMetricEvent(event, { env });
920
- }
921
- return run?.state || null;
922
- }
923
-
924
682
  function parseCliArgs(argv) {
925
683
  const command = argv[2];
926
684
  const args = { _: [] };
@@ -942,20 +700,11 @@ function parseCliArgs(argv) {
942
700
  function contextFromCliArgs(args) {
943
701
  const context = {
944
702
  runId: args['run-id'],
945
- rootIntent: args['root-intent'],
946
- productType: args['product-type'],
947
- productName: args['product-name']
703
+ historyCount: args['history-count']
948
704
  };
949
705
  return context;
950
706
  }
951
707
 
952
- function timestampFromCliArgs(args) {
953
- if (args['timestamp-ms'] === undefined) return Date.now();
954
- const value = Number(args['timestamp-ms']);
955
- if (!Number.isSafeInteger(value) || value <= 0) throw new Error('--timestamp-ms 必须是正整数毫秒时间戳');
956
- return value;
957
- }
958
-
959
708
  function assertCliArgs(args, allowedOptions, positionalCount) {
960
709
  for (const name of Object.keys(args)) {
961
710
  if (name !== '_' && !allowedOptions.has(name)) throw new Error(`不支持参数 --${name}`);
@@ -968,64 +717,42 @@ function createRunState(args, env = process.env, nowMs = Date.now()) {
968
717
  if (!allowedRootIntents.has(rootIntent)) throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
969
718
  const productType = canonicalProductType(args['product-type']);
970
719
  if (!productType) return null;
971
- const runId = platformProjectRunId(env) || safeRunId(args['run-id']) || crypto.randomUUID();
720
+ const runId = crypto.randomUUID();
972
721
  return {
973
- schema_ver: 1,
722
+ schema_ver: 2,
974
723
  run_id: runId,
975
724
  root_intent: rootIntent,
976
725
  product_type: productType,
977
- user_input_counts: initialUserInputCounts(nowMs),
726
+ lifecycle: 'ACTIVE',
727
+ clarify_intent_recorded: false,
728
+ counter_count: 0,
729
+ input_hashes: [],
730
+ stage_events: {},
978
731
  created_at: nowMs,
979
- updated_at: nowMs
732
+ updated_at: nowMs,
733
+ terminal_at: null
980
734
  };
981
735
  }
982
736
 
983
- function compatibleProductType(existingProductType, requestedProductType) {
984
- const existing = canonicalProductType(existingProductType);
985
- const requested = canonicalProductType(requestedProductType);
986
- if (!existing || !requested) return false;
987
- if (existing === 'UNKNOWN' || requested === 'UNKNOWN') return false;
988
- return existing === requested;
989
- }
990
-
991
- function runStateCompatibleWithEventContext(state, context = {}) {
992
- if (!state) return true;
993
- const requestedRootIntent = context.rootIntent || context.root_intent;
994
- if (requestedRootIntent && state.root_intent && state.root_intent !== requestedRootIntent) return false;
995
- const requestedProductType = canonicalProductType(context.productType || context.product_type);
996
- if (requestedProductType && !compatibleProductType(state.product_type, requestedProductType)) return false;
997
- return true;
998
- }
999
-
1000
- function mergeProductType(existingProductType, requestedProductType) {
1001
- const existing = canonicalProductType(existingProductType) || 'UNKNOWN';
1002
- const requested = canonicalProductType(requestedProductType) || 'UNKNOWN';
1003
- if (existing === 'UNKNOWN' && requested !== 'UNKNOWN') return requested;
1004
- if (requested === 'UNKNOWN' && existing !== 'UNKNOWN') return existing;
1005
- return requested;
1006
- }
1007
-
1008
737
  function reusableRunState(existing, requestedRootIntent, requestedProductType) {
1009
- if (!existing || existing.terminal) return false;
738
+ if (!existing || existing.lifecycle !== 'ACTIVE') return false;
1010
739
  if (existing.root_intent !== requestedRootIntent) return false;
1011
740
  const existingProductType = canonicalProductType(existing.product_type);
1012
741
  const nextProductType = canonicalProductType(requestedProductType);
1013
742
  if (!existingProductType || !nextProductType) return false;
1014
- if (existingProductType === 'UNKNOWN' || nextProductType === 'UNKNOWN') return false;
1015
743
  return existingProductType === nextProductType;
1016
744
  }
1017
745
 
1018
746
  function createOrReuseRunState(args, env = process.env, nowMs = Date.now()) {
1019
747
  const rootIntent = args['root-intent'];
1020
748
  if (!allowedRootIntents.has(rootIntent)) throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
1021
- const requestedProductType = canonicalProductType(args['product-type']) || 'UNKNOWN';
1022
- const requestedRunId = platformProjectRunId(env) || safeRunId(args['run-id']);
749
+ const requestedProductType = canonicalProductType(args['product-type']);
750
+ if (!requestedProductType) return null;
1023
751
  const existing = readRunState(env);
1024
- if (reusableRunState(existing, rootIntent, requestedProductType) && (!requestedRunId || existing.run_id === requestedRunId)) {
752
+ if (reusableRunState(existing, rootIntent, requestedProductType)
753
+ && (telemetryEnabled(env) ? existing.clarify_intent_recorded === true : true)) {
1025
754
  const state = {
1026
755
  ...existing,
1027
- root_intent: rootIntent,
1028
- product_type: mergeProductType(existing.product_type, requestedProductType),
1029
756
  updated_at: nowMs
1030
757
  };
1031
758
  return {
@@ -1037,37 +764,29 @@ function createOrReuseRunState(args, env = process.env, nowMs = Date.now()) {
1037
764
  return state ? { reused: false, state } : null;
1038
765
  }
1039
766
 
1040
- function withCompletedStage(state, stage, nowMs = Date.now()) {
1041
- const progress = normalizeStageProgress(state?.stage_progress);
1042
- if (progress.completed_stages.includes(stage)) return state;
1043
- return {
1044
- ...state,
1045
- stage_progress: {
1046
- completed_stages: [...progress.completed_stages, stage],
1047
- updated_at: nowMs
1048
- },
1049
- updated_at: nowMs
1050
- };
1051
- }
1052
-
1053
- export function recordTelemetryUserInput(inputId, env = process.env, nowMs = Date.now()) {
767
+ export function recordTelemetryUserInput(inputId, env = process.env, nowMs = Date.now(), runId = '') {
1054
768
  if (!telemetryEnabled(env)) return null;
1055
- const normalizedInputId = normalizeUserInputId(inputId);
1056
- if (!normalizedInputId) throw new Error('用户输入计数必须提供本地幂等 --input-id,且不得包含用户输入原文或敏感信息');
1057
769
  const transaction = withRunStateLock(env, () => {
1058
770
  const existing = readRunState(env);
1059
- if (!existing?.run_id || existing.terminal) return null;
1060
- const counts = normalizeUserInputCounts(existing.user_input_counts);
771
+ const explicitRunId = safeRunId(runId);
772
+ if (!existing?.run_id || existing.lifecycle !== 'ACTIVE' || !explicitRunId || explicitRunId !== existing.run_id) return null;
773
+ const normalizedInputId = normalizeUserInputId(inputId);
774
+ if (!normalizedInputId) throw new Error('用户输入计数必须提供不含用户原文或敏感信息的本地幂等 --input-id');
775
+ const count = parseNonNegativeInteger(existing.counter_count);
776
+ if (count === null) return null;
777
+ const hashes = normalizeUserInputHashes(existing.input_hashes);
1061
778
  const inputHash = userInputHash(normalizedInputId, existing.run_id);
1062
779
  if (!inputHash) throw new Error('用户输入计数 input-id 无法生成本地幂等键');
1063
- if (counts.input_hashes.includes(inputHash)) return counts;
1064
- const nextCounts = addUserInputCount(counts, normalizedInputId, existing.run_id, nowMs);
780
+ if (hashes.includes(inputHash)) return { user_input_after_clarify_intent: count };
1065
781
  const next = {
1066
782
  ...existing,
1067
- user_input_counts: nextCounts,
783
+ counter_count: count + 1,
784
+ input_hashes: [...hashes, inputHash],
1068
785
  updated_at: nowMs
1069
786
  };
1070
- return persistRunState(next, env) ? nextCounts : null;
787
+ return persistRunState(next, env)
788
+ ? { user_input_after_clarify_intent: next.counter_count }
789
+ : null;
1071
790
  });
1072
791
  if (!transaction.acquired) {
1073
792
  recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to lock local telemetry run state', env);
@@ -1076,38 +795,116 @@ export function recordTelemetryUserInput(inputId, env = process.env, nowMs = Dat
1076
795
  return transaction.value;
1077
796
  }
1078
797
 
1079
- function updateExistingRunStateFromStageEvent(event, env = process.env) {
1080
- if (!event || !stageRules.has(event.stage)) return false;
798
+ async function deliverPersistedStageEvent(event, env = process.env, stream = process.stderr) {
799
+ writeMetricEventMarker(event, stream);
800
+ const queuedFile = writeOutboxRecord(event, env);
801
+ if (!queuedFile) {
802
+ recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'outbox write failed', env);
803
+ return false;
804
+ }
805
+ writeUploadStatus({ status: 'pending', attempted: 0, sent: 0, retained: 1 }, env);
806
+ if (!telemetryUploadEnabled(env)) return true;
807
+ await flushOutbox(env, { limit: 1, targetFile: queuedFile, timeoutMs: uploadTimeoutMs });
808
+ if (hasPendingOutboxRecords(env)) spawnOutboxFlush(env);
809
+ return true;
810
+ }
811
+
812
+ export async function recordStageEvent(stage, context = {}, env = process.env, nowMs = Date.now(), stream = process.stderr) {
813
+ if (!telemetryEnabled(env)) return null;
814
+ if (!stageRules.has(stage)) throw new Error('stage 不在公开 telemetry stage 列表中');
815
+ const runId = safeRunId(context.runId || context.run_id);
816
+ const historyCount = parseHistoryCount(context.historyCount ?? context.history_count, { strict: true });
1081
817
  const transaction = withRunStateLock(env, () => {
1082
818
  const existing = readRunState(env);
1083
- if (!existing) return true;
1084
- if (existing.run_id !== event.run_id) return true;
1085
- if (!runStateCompatibleWithEventContext(existing, {
1086
- rootIntent: event.root_intent,
1087
- productType: event.product_type
1088
- })) return true;
1089
- const counts = normalizeUserInputCounts(existing.user_input_counts);
1090
- const route = routeStages(existing.root_intent, existing.product_type);
1091
- if (route.length > 0 && !route.includes(event.stage) && !summaryStages.has(event.stage)) return true;
1092
- const next = withCompletedStage({
819
+ if (!existing || existing.lifecycle !== 'ACTIVE') return { kind: 'noop' };
820
+ if (!runId || runId !== existing.run_id) return { kind: 'noop' };
821
+ if (!existing.clarify_intent_recorded || !stageAlreadyRecorded(existing, 'clarify_intent')) return { kind: 'noop' };
822
+ if (!legalStageForState(existing, stage) || stage === 'clarify_intent') return { kind: 'noop' };
823
+ if (stageAlreadyRecorded(existing, stage)) return { kind: 'duplicate' };
824
+ if (parseNonNegativeInteger(existing.counter_count) === null) return { kind: 'noop' };
825
+ const event = eventFromState(stage, existing, historyCount, nowMs);
826
+ if (!event) return { kind: 'noop' };
827
+ const closesRun = shouldCloseRunAfterStage(event, existing);
828
+ const next = {
1093
829
  ...existing,
1094
- schema_ver: 1,
1095
- run_id: event.run_id,
1096
- root_intent: existing.root_intent || event.root_intent || '',
1097
- product_type: event.product_type && event.product_type !== 'UNKNOWN'
1098
- ? event.product_type
1099
- : existing.product_type || 'UNKNOWN',
1100
- created_at: existing.created_at || Date.now(),
1101
- updated_at: Date.now(),
1102
- user_input_counts: counts
1103
- }, event.stage, Number.isFinite(Number(event.timestamp)) ? Number(event.timestamp) : Date.now());
1104
- return persistRunState(next, env);
830
+ stage_events: {
831
+ ...existing.stage_events,
832
+ [stage]: { event_id: event.event_id, generated_at: nowMs }
833
+ },
834
+ lifecycle: closesRun ? 'CLOSED' : existing.lifecycle,
835
+ terminal_at: closesRun ? nowMs : existing.terminal_at,
836
+ updated_at: nowMs
837
+ };
838
+ return persistRunState(next, env) ? { kind: 'created', event } : { kind: 'lost' };
1105
839
  });
1106
840
  if (!transaction.acquired) {
1107
841
  recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to lock local telemetry run state', env);
1108
- return false;
842
+ return null;
1109
843
  }
1110
- return transaction.value;
844
+ const result = transaction.value;
845
+ if (result?.kind !== 'created') return null;
846
+ await deliverPersistedStageEvent(result.event, env, stream);
847
+ return result.event;
848
+ }
849
+
850
+ export async function startTelemetryRun(rootIntent, productType, _inputId = '', {
851
+ env = process.env,
852
+ nowMs = Date.now(),
853
+ stream = process.stderr
854
+ } = {}) {
855
+ if (!allowedRootIntents.has(rootIntent)) {
856
+ if (!rootIntent) return null;
857
+ throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
858
+ }
859
+ const canonicalProduct = canonicalProductType(productType);
860
+ if (!canonicalProduct) return null;
861
+ const enabled = telemetryEnabled(env);
862
+ const transaction = withRunStateLock(env, () => {
863
+ const run = createOrReuseRunState({
864
+ 'root-intent': rootIntent,
865
+ 'product-type': canonicalProduct
866
+ }, env, nowMs);
867
+ if (!run?.state) return null;
868
+ if (run.reused) return persistRunState(run.state, env) ? { ...run, event: null } : null;
869
+ let state = run.state;
870
+ let event = null;
871
+ if (enabled) {
872
+ event = eventFromState('clarify_intent', state, 0, nowMs);
873
+ if (!event) return null;
874
+ state = {
875
+ ...state,
876
+ clarify_intent_recorded: true,
877
+ stage_events: {
878
+ clarify_intent: { event_id: event.event_id, generated_at: nowMs }
879
+ }
880
+ };
881
+ } else {
882
+ state = {
883
+ schema_ver: 2,
884
+ run_id: state.run_id,
885
+ root_intent: state.root_intent,
886
+ product_type: state.product_type,
887
+ lifecycle: 'ACTIVE',
888
+ telemetry_disabled: true,
889
+ created_at: state.created_at,
890
+ updated_at: nowMs,
891
+ terminal_at: null
892
+ };
893
+ }
894
+ return persistRunState(state, env) ? { ...run, state, event } : null;
895
+ });
896
+ if (!transaction.acquired || !transaction.value?.state) {
897
+ recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to persist local telemetry run state', env);
898
+ return null;
899
+ }
900
+ const { state, event } = transaction.value;
901
+ env.ALIPAY_AIPAY_TELEMETRY_RUN_ID = state.run_id;
902
+ if (event) env.ALIPAY_AIPAY_TELEMETRY_HISTORY_COUNT = '0';
903
+ if (stream && typeof stream.write === 'function') {
904
+ stream.write(`${internalPrefix}RUN_ID=${state.run_id}\n`);
905
+ }
906
+ if (event) await deliverPersistedStageEvent(event, env, stream);
907
+ return state;
1111
908
  }
1112
909
 
1113
910
  function normalizeOS(platform = process.platform) {
@@ -1156,7 +953,19 @@ function platformFromEnv(env = process.env) {
1156
953
  }
1157
954
 
1158
955
  function modelFromEnv(env = process.env) {
1159
- return safeDimension(env.AIPAY_MODEL || 'unknown');
956
+ const raw = env.ALIPAY_AIPAY_MODEL || env.AIPAY_MODEL || '';
957
+ const normalized = String(raw).trim().toLowerCase();
958
+ if (!normalized) return 'unknown';
959
+
960
+ // Keep model telemetry at a coarse family level; variants and reasoning modes are not needed here.
961
+ if (/^(?:openai\/)?gpt[-_ ]?5(?:[.\-_ ]|$)/u.test(normalized)) return 'gpt-5';
962
+ if (/^(?:openai\/)?gpt[-_ ]?4(?:[.\-_ ]|$)/u.test(normalized)) return 'gpt-4';
963
+ if (/^claude(?:[.\-_ ]|$)/u.test(normalized)) return 'claude';
964
+ if (/^gemini(?:[.\-_ ]|$)/u.test(normalized)) return 'gemini';
965
+ if (/^qwen(?:[.\-_ ]|$)/u.test(normalized)) return 'qwen';
966
+ if (/^deepseek(?:[.\-_ ]|$)/u.test(normalized)) return 'deepseek';
967
+ if (/^llama(?:[.\-_ ]|$)/u.test(normalized)) return 'llama';
968
+ return 'unknown';
1160
969
  }
1161
970
 
1162
971
  function skillVersionFromDisk() {
@@ -1458,6 +1267,7 @@ export function sendSyncLogReportDetailed(report, { timeoutMs = uploadTimeoutMs
1458
1267
  }
1459
1268
 
1460
1269
  function outboxFiles(env = process.env, { targetFile = '', limit = 0 } = {}) {
1270
+ if (!telemetryEnabled(env)) return [];
1461
1271
  try {
1462
1272
  const dir = outboxDir(env);
1463
1273
  recoverStaleOutboxClaims(env);
@@ -1496,6 +1306,7 @@ function outboxRecordEntries(dir) {
1496
1306
  }
1497
1307
 
1498
1308
  function pruneOutbox(env = process.env, nowMs = Date.now()) {
1309
+ if (!telemetryEnabled(env)) return;
1499
1310
  try {
1500
1311
  const dir = outboxDir(env);
1501
1312
  let entries = outboxRecordEntries(dir);
@@ -1583,6 +1394,7 @@ function readOutboxRecord(file) {
1583
1394
  }
1584
1395
 
1585
1396
  function hasPendingOutboxRecords(env = process.env) {
1397
+ if (!telemetryEnabled(env)) return false;
1586
1398
  try {
1587
1399
  const dir = outboxDir(env);
1588
1400
  recoverStaleOutboxClaims(env);
@@ -1592,11 +1404,12 @@ function hasPendingOutboxRecords(env = process.env) {
1592
1404
  return name.endsWith('.json') || name.includes('.json.sending.');
1593
1405
  });
1594
1406
  } catch {
1595
- return true;
1407
+ return false;
1596
1408
  }
1597
1409
  }
1598
1410
 
1599
1411
  function writeUploadStatus(status, env = process.env) {
1412
+ if (!telemetryEnabled(env)) return false;
1600
1413
  return writePrivateJson(uploadStatusPath(env), { schema_ver: 1, updated_at: Date.now(), ...status });
1601
1414
  }
1602
1415
 
@@ -1624,6 +1437,9 @@ function networkUploadFailure(status) {
1624
1437
  }
1625
1438
 
1626
1439
  export function permissionedFlushDecision(env = process.env) {
1440
+ if (!telemetryUploadEnabled(env)) {
1441
+ return { needed: false, outbox_count: 0, error_code: '', status: 'disabled' };
1442
+ }
1627
1443
  const outboxCount = outboxFiles(env).length;
1628
1444
  const status = readUploadStatus(env);
1629
1445
  const errorCode = networkUploadFailure(status);
@@ -1633,7 +1449,7 @@ export function permissionedFlushDecision(env = process.env) {
1633
1449
  error_code: errorCode || safeDimension(status?.error_code || '', '', 80),
1634
1450
  status: safeDimension(status?.status || 'unknown', 'unknown', 80)
1635
1451
  };
1636
- if (!telemetryUploadEnabled(env) || outboxCount <= 0 || !errorCode) {
1452
+ if (outboxCount <= 0 || !errorCode) {
1637
1453
  return result;
1638
1454
  }
1639
1455
  result.needed = true;
@@ -1805,14 +1621,11 @@ export async function runTelemetryCli(argv = process.argv) {
1805
1621
  try {
1806
1622
  const { command, args } = parseCliArgs(argv);
1807
1623
  if (command === 'start-run') {
1808
- assertCliArgs(args, new Set(['root-intent', 'product-type', 'run-id', 'input-id']), 0);
1624
+ assertCliArgs(args, new Set(['root-intent', 'product-type']), 0);
1809
1625
  if (!allowedRootIntents.has(args['root-intent'])) {
1810
1626
  throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
1811
1627
  }
1812
- await startTelemetryRun(args['root-intent'], args['product-type'], args['input-id'], {
1813
- runId: args['run-id'],
1814
- env: process.env
1815
- });
1628
+ await startTelemetryRun(args['root-intent'], args['product-type'], '', { env: process.env });
1816
1629
  return 0;
1817
1630
  }
1818
1631
  if (command === 'current-run-id') {
@@ -1831,10 +1644,13 @@ export async function runTelemetryCli(argv = process.argv) {
1831
1644
  return 0;
1832
1645
  }
1833
1646
  if (command === 'count-user-input' && args._.length === 0) {
1834
- assertCliArgs(args, new Set(['input-id']), 0);
1835
- const counts = recordTelemetryUserInput(args['input-id'], process.env, Date.now());
1647
+ assertCliArgs(args, new Set(['run-id', 'input-id']), 0);
1648
+ if (typeof args['input-id'] !== 'string' || !args['input-id']) {
1649
+ throw new Error('缺少参数 --input-id');
1650
+ }
1651
+ const counts = recordTelemetryUserInput(args['input-id'], process.env, Date.now(), args['run-id']);
1836
1652
  if (counts) {
1837
- process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE=${JSON.stringify(publicUserInputCountState(counts))}\n`);
1653
+ process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE=${JSON.stringify(counts)}\n`);
1838
1654
  } else {
1839
1655
  process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE_MISSING=true\n`);
1840
1656
  }
@@ -1842,12 +1658,14 @@ export async function runTelemetryCli(argv = process.argv) {
1842
1658
  }
1843
1659
  if (command === 'upload-status' && args._.length === 0) {
1844
1660
  assertCliArgs(args, new Set(), 0);
1661
+ if (!telemetryEnabled(process.env)) return 0;
1845
1662
  const status = readUploadStatus(process.env) || { schema_ver: 1, status: 'unknown' };
1846
1663
  process.stderr.write(`${internalPrefix}TELEMETRY_UPLOAD_STATUS=${JSON.stringify(status)}\n`);
1847
1664
  return 0;
1848
1665
  }
1849
1666
  if (command === 'permissioned-flush-status' && args._.length === 0) {
1850
1667
  assertCliArgs(args, new Set(), 0);
1668
+ if (!telemetryEnabled(process.env)) return 0;
1851
1669
  const decision = permissionedFlushDecision(process.env);
1852
1670
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_STATUS=${JSON.stringify(decision)}\n`);
1853
1671
  if (decision.needed) {
@@ -1857,6 +1675,7 @@ export async function runTelemetryCli(argv = process.argv) {
1857
1675
  }
1858
1676
  if (command === 'permissioned-flush-needed' && args._.length === 0) {
1859
1677
  assertCliArgs(args, new Set(), 0);
1678
+ if (!telemetryEnabled(process.env)) return 0;
1860
1679
  const decision = permissionedFlushDecision(process.env);
1861
1680
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
1862
1681
  if (decision.needed) process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
@@ -1864,13 +1683,13 @@ export async function runTelemetryCli(argv = process.argv) {
1864
1683
  }
1865
1684
  if (command === 'flush-outbox') {
1866
1685
  assertCliArgs(args, new Set(), 0);
1867
- if (!telemetryEnabled(process.env)) return 0;
1686
+ if (!telemetryUploadEnabled(process.env)) return 0;
1868
1687
  await runPermissionedOutboxFlush(process.env, { spawnBackground: true });
1869
1688
  return 0;
1870
1689
  }
1871
1690
  if (command === 'flush-outbox-worker') {
1872
1691
  assertCliArgs(args, new Set(), 0);
1873
- if (!telemetryEnabled(process.env)) return 0;
1692
+ if (!telemetryUploadEnabled(process.env)) return 0;
1874
1693
  const timeout = Number(process.env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || uploadTimeoutMs);
1875
1694
  const delay = parseNonNegativeInteger(process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_DELAY_MS);
1876
1695
  if (delay && delay > 0) sleepSync(Math.min(delay, 30000));
@@ -1881,29 +1700,23 @@ export async function runTelemetryCli(argv = process.argv) {
1881
1700
  return 0;
1882
1701
  }
1883
1702
  if (!telemetryEnabled(process.env)) return 0;
1884
- if (command === 'stage-async') {
1885
- assertCliArgs(args, new Set([
1886
- 'run-id',
1887
- 'root-intent',
1888
- 'product-type',
1889
- 'product-name'
1890
- ]), 1);
1891
- emitStageMetricAsync(args._[0], contextFromCliArgs(args), process.env);
1892
- return 0;
1893
- }
1894
1703
  if (command !== 'stage' || args._.length !== 1) {
1895
1704
  emitTelemetryError(new Error('invalid telemetry arguments'));
1896
1705
  return 0;
1897
1706
  }
1898
1707
  assertCliArgs(args, new Set([
1899
1708
  'run-id',
1900
- 'root-intent',
1901
- 'product-type',
1902
- 'product-name',
1903
- 'timestamp-ms'
1709
+ 'history-count'
1904
1710
  ]), 1);
1905
- const event = stageMetricEvent(args._[0], contextFromCliArgs(args), timestampFromCliArgs(args));
1906
- emitMetricEvent(event);
1711
+ if (typeof args['run-id'] !== 'string' || !args['run-id']) throw new Error('缺少参数 --run-id');
1712
+ if (!Object.prototype.hasOwnProperty.call(args, 'history-count')) throw new Error('缺少参数 --history-count');
1713
+ await recordStageEvent(
1714
+ args._[0],
1715
+ contextFromCliArgs(args),
1716
+ process.env,
1717
+ Date.now(),
1718
+ process.stderr
1719
+ );
1907
1720
  return 0;
1908
1721
  } catch (error) {
1909
1722
  emitTelemetryError(error);