@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.
- package/package.json +1 -1
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +10 -8
- package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +1 -1
- package/skill/references/integration/modules/scripts/sandbox_config.sh +2 -2
- package/skill/references/normal/full-process-routing.md +3 -1
- package/skill/references/normal/integration-worker.md +11 -5
- package/skill/references/normal/scripts/common.sh +12 -14
- package/skill/references/normal/scripts/integration_message_runner.mjs +3 -8
- package/skill/references/normal/scripts/interaction_reply.mjs +1 -2
- package/skill/references/normal/scripts/onboarding_discovery_runner.mjs +2 -2
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +8 -10
- package/skill/references/normal/scripts/onboarding_recovery_runner.mjs +1 -2
- package/skill/references/normal/scripts/runtime.mjs +40 -6
- package/skill/references/normal/scripts/telemetry.mjs +306 -493
- package/skill/references/normal/telemetry.md +60 -4
- package/skill/references/onboarding/flow.md +27 -25
- package/skill/references/onboarding/modules/scripts/app.sh +1 -1
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +2 -2
- package/skill/references/onboarding/modules/scripts/auth.sh +4 -4
- package/skill/references/onboarding/modules/scripts/service.sh +2 -2
- package/skill/references/runtime-entry.md +8 -6
|
@@ -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'
|
|
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 =
|
|
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
|
|
170
|
-
if (!
|
|
171
|
-
|
|
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'
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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) ||
|
|
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
|
-
|
|
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
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
666
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
if (!rootIntent) return
|
|
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:
|
|
734
|
-
|
|
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
|
-
|
|
740
|
-
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (
|
|
750
|
-
const
|
|
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:
|
|
633
|
+
event_id: eventId,
|
|
753
634
|
skill: 'alipay-aipay',
|
|
754
635
|
skill_version: skillVersionFromDisk(),
|
|
755
|
-
run_id:
|
|
756
|
-
stage
|
|
636
|
+
run_id: state.run_id,
|
|
637
|
+
stage,
|
|
757
638
|
timestamp: nowMs,
|
|
758
|
-
flow:
|
|
759
|
-
product_type:
|
|
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
|
|
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
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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.
|
|
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
|
|
878
|
-
|
|
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
|
-
|
|
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 =
|
|
720
|
+
const runId = crypto.randomUUID();
|
|
972
721
|
return {
|
|
973
|
-
schema_ver:
|
|
722
|
+
schema_ver: 2,
|
|
974
723
|
run_id: runId,
|
|
975
724
|
root_intent: rootIntent,
|
|
976
725
|
product_type: productType,
|
|
977
|
-
|
|
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.
|
|
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'])
|
|
1022
|
-
|
|
749
|
+
const requestedProductType = canonicalProductType(args['product-type']);
|
|
750
|
+
if (!requestedProductType) return null;
|
|
1023
751
|
const existing = readRunState(env);
|
|
1024
|
-
if (reusableRunState(existing, rootIntent, requestedProductType)
|
|
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
|
|
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
|
-
|
|
1060
|
-
|
|
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 (
|
|
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
|
-
|
|
783
|
+
counter_count: count + 1,
|
|
784
|
+
input_hashes: [...hashes, inputHash],
|
|
1068
785
|
updated_at: nowMs
|
|
1069
786
|
};
|
|
1070
|
-
return persistRunState(next, env)
|
|
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
|
|
1080
|
-
|
|
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
|
|
1084
|
-
if (
|
|
1085
|
-
if (!
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const next =
|
|
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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
|
842
|
+
return null;
|
|
1109
843
|
}
|
|
1110
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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'
|
|
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'],
|
|
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
|
-
|
|
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(
|
|
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 (!
|
|
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 (!
|
|
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
|
-
'
|
|
1901
|
-
'product-type',
|
|
1902
|
-
'product-name',
|
|
1903
|
-
'timestamp-ms'
|
|
1709
|
+
'history-count'
|
|
1904
1710
|
]), 1);
|
|
1905
|
-
|
|
1906
|
-
|
|
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);
|