@alipay/alipay-aipay 1.6.2 → 1.6.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alipay/alipay-aipay",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "支付宝 AI 付 Skill 安装包 - 优先安装 Agent Skill,并在 macOS/Linux 后台准备用户级 CLI 工具",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 1.6.2
1
+ 1.6.3
@@ -1231,7 +1231,7 @@ function runProductClarify() {
1231
1231
  function runProductUnsupported(args) {
1232
1232
  return renderMessage('product.unsupported', 'DEFAULT', {
1233
1233
  productName: required(args, 'product-name')
1234
- }, terminalMetricContextFromArgs(args));
1234
+ }, terminalMetricContextFromArgs(args, { terminalReason: 'unsupported_product' }));
1235
1235
  }
1236
1236
 
1237
1237
  function runWindowsSandboxSetup() {
@@ -1313,6 +1313,12 @@ function checklistInputFromArgs(args) {
1313
1313
  return input;
1314
1314
  }
1315
1315
 
1316
+ function terminalReasonForChecklist(input) {
1317
+ if (input.overallResult === '通过') return 'completed';
1318
+ if (input.overallResult === '未通过' || input.blockingDefectState === 'PRESENT') return 'integration_blocked';
1319
+ return 'partial_with_remaining_actions';
1320
+ }
1321
+
1316
1322
  async function runChecklistResult(args) {
1317
1323
  const variant = required(args, 'variant');
1318
1324
  if (!['AIPAY', 'WEBPAY', 'APPPAY'].includes(variant)) throw new Error('variant 非法,仅支持 AIPAY、WEBPAY、APPPAY');
@@ -1360,7 +1366,10 @@ async function runChecklistResult(args) {
1360
1366
  emitTerminalMetric({
1361
1367
  rootIntent: 'integration_only',
1362
1368
  productType: variant,
1363
- ...terminalMetricContextFromArgs(args, { rootIntent: 'integration_only' })
1369
+ ...terminalMetricContextFromArgs(args, {
1370
+ rootIntent: 'integration_only',
1371
+ terminalReason: terminalReasonForChecklist(input)
1372
+ })
1364
1373
  });
1365
1374
  }
1366
1375
  return rendered;
@@ -157,7 +157,7 @@ function runPlatformEntry(args = {}) {
157
157
  }
158
158
  return renderMessage('onboarding.windows.redirect', 'DEFAULT', {
159
159
  officialUrl: buildWindowsOnboardingRedirectUrl(runContext.run_id)
160
- }, terminalMetricContextFromArgs(args));
160
+ }, terminalMetricContextFromArgs(args, { terminalReason: 'unsupported_platform' }));
161
161
  }
162
162
 
163
163
  function parseJsonArg(args, name, label) {
@@ -178,7 +178,12 @@ function runEnterpriseUnsupported(args) {
178
178
  if (!['ONBOARDING_ONLY', 'FULL_PROCESS'].includes(variant)) {
179
179
  throw new Error('variant 非法,仅支持 ONBOARDING_ONLY、FULL_PROCESS');
180
180
  }
181
- return renderMessage('onboarding.enterprise.unsupported', variant, {}, terminalMetricContextFromArgs(args));
181
+ return renderMessage(
182
+ 'onboarding.enterprise.unsupported',
183
+ variant,
184
+ {},
185
+ terminalMetricContextFromArgs(args, { terminalReason: 'enterprise_account_unsupported' })
186
+ );
182
187
  }
183
188
 
184
189
  async function runMccClarify(args) {
@@ -751,10 +756,18 @@ function closeoutInputFromArgs(args) {
751
756
  async function runCloseout(args) {
752
757
  const variables = closeoutInputFromArgs(args);
753
758
  const rendered = renderMessage('process.partial_result', 'DEFAULT', variables);
759
+ const terminalReason = args['root-intent'] === 'full_process' && variables.integrationResult !== '已完成'
760
+ ? 'partial_with_remaining_actions'
761
+ : variables.processResultTitle === '支付产品-产品开通已完成'
762
+ ? 'completed'
763
+ : 'partial_with_remaining_actions';
754
764
  emitTerminalMetric({
755
765
  productName: variables.productName,
756
766
  rootIntent: args['root-intent'],
757
- ...terminalMetricContextFromArgs(args, { rootIntent: args['root-intent'] })
767
+ ...terminalMetricContextFromArgs(args, {
768
+ rootIntent: args['root-intent'],
769
+ terminalReason
770
+ })
758
771
  });
759
772
  return rendered;
760
773
  }
@@ -22,17 +22,26 @@ const terminalForegroundUploadTimeoutMs = 1200;
22
22
  const terminalForegroundUploadMaxAttempts = 2;
23
23
  const terminalForegroundUploadRetryDelayMs = 300;
24
24
  const terminalBackgroundRetryDelayMs = 1000;
25
+ const permissionedForegroundFlushLimit = 5;
25
26
  const terminalCountArgNames = Object.freeze([
26
27
  'user-input-count-total',
27
28
  'user-input-count-after-flow-start',
28
29
  'flow-start-observed'
29
30
  ]);
31
+ const terminalReasonValues = new Set([
32
+ 'completed',
33
+ 'partial_with_remaining_actions',
34
+ 'integration_blocked',
35
+ 'unsupported_product',
36
+ 'unsupported_platform',
37
+ 'enterprise_account_unsupported',
38
+ 'unknown'
39
+ ]);
30
40
  const initialUserInputId = 'run.start';
31
41
  const outboxFlushLimit = 50;
32
42
  const outboxClaimTtlMs = 5 * 60 * 1000;
33
43
  const outboxMaxRecords = 1000;
34
44
  const outboxMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
35
- const permissionedFlushTtlMs = 12 * 60 * 60 * 1000;
36
45
  const platformProjectIdEnvKeys = Object.freeze(['COZE_PROJECT_ID', 'MEOO_PROJECT_ID']);
37
46
  const sessionStateEnvKeys = Object.freeze([
38
47
  'CODEX_THREAD_ID',
@@ -227,21 +236,12 @@ function uploadStatusPath(env = process.env) {
227
236
  return path.join(stateDir(env), 'upload-status.json');
228
237
  }
229
238
 
230
- function permissionedFlushStatePath(env = process.env) {
231
- if (env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE) {
232
- return env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE;
233
- }
234
- const source = stableStateSource(env) || (Object.keys(env).length > 0 ? readRunState(env)?.run_id : '') || '';
235
- return path.join(stateDir(env), `${stateKeyFromSource(source)}.permissioned-flush.json`);
236
- }
237
-
238
239
  export function telemetryStoragePaths(env = process.env) {
239
240
  return {
240
241
  state_dir: stateDir(env),
241
242
  run_state_file: runStatePath(env),
242
243
  outbox_dir: outboxDir(env),
243
- upload_status_file: uploadStatusPath(env),
244
- permissioned_flush_state_file: permissionedFlushStatePath(env)
244
+ upload_status_file: uploadStatusPath(env)
245
245
  };
246
246
  }
247
247
 
@@ -417,6 +417,12 @@ function parseBooleanFlag(value) {
417
417
  return null;
418
418
  }
419
419
 
420
+ function normalizeTerminalReason(value) {
421
+ if (typeof value !== 'string' || !value) return 'unknown';
422
+ const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, '_');
423
+ return terminalReasonValues.has(normalized) ? normalized : '';
424
+ }
425
+
420
426
  function normalizeUserInputId(value) {
421
427
  if (typeof value !== 'string') return '';
422
428
  const text = value.trim();
@@ -518,11 +524,14 @@ function terminalExtInfoFromPayload(payload = {}, env = process.env) {
518
524
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
519
525
  const counts = validateTerminalCountPayload(payload, env);
520
526
  if (!counts) return '';
527
+ const terminalReason = normalizeTerminalReason(payload.terminalReason ?? payload.terminal_reason);
528
+ if (!terminalReason) return '';
521
529
  return JSON.stringify({
522
530
  user_input_count_total: counts.total,
523
531
  user_input_count_after_flow_start: counts.afterFlowStart,
524
532
  flow_start_observed: counts.flowStartObserved,
525
- root_intent: counts.rootIntent
533
+ root_intent: counts.rootIntent,
534
+ terminal_reason: terminalReason
526
535
  });
527
536
  }
528
537
 
@@ -685,6 +694,11 @@ export function terminalMetricContextFromArgs(args, context = {}) {
685
694
  const value = args[name];
686
695
  return typeof value === 'string' && value.length > 0 ? value : undefined;
687
696
  };
697
+ const terminalReason = normalizeTerminalReason(
698
+ optional('terminal-reason') ?? context.terminalReason ?? context.terminal_reason
699
+ );
700
+ if (!terminalReason) throw new Error('--terminal-reason 不在允许枚举中');
701
+ const reasonContext = { terminalReason };
688
702
  const hasCountArg = terminalCountArgNames.some((name) => Object.prototype.hasOwnProperty.call(args, name));
689
703
  if (hasCountArg) {
690
704
  const missing = terminalCountArgNames.filter((name) => optional(name) === undefined);
@@ -696,9 +710,11 @@ export function terminalMetricContextFromArgs(args, context = {}) {
696
710
  rootIntent: args['root-intent'] || args.rootIntent || args.root_intent || context.rootIntent || context.root_intent
697
711
  }, process.env, { strict: true });
698
712
  return {
713
+ rootIntent: counts.rootIntent,
699
714
  userInputCountTotal: counts.total,
700
715
  userInputCountAfterFlowStart: counts.afterFlowStart,
701
- flowStartObserved: counts.flowStartObserved
716
+ flowStartObserved: counts.flowStartObserved,
717
+ ...reasonContext
702
718
  };
703
719
  }
704
720
  const state = readRunState(process.env);
@@ -712,9 +728,11 @@ export function terminalMetricContextFromArgs(args, context = {}) {
712
728
  }, process.env);
713
729
  if (!fallbackCounts) return {};
714
730
  return {
731
+ rootIntent: fallbackCounts.rootIntent,
715
732
  userInputCountTotal: fallbackCounts.total,
716
733
  userInputCountAfterFlowStart: fallbackCounts.afterFlowStart,
717
- flowStartObserved: fallbackCounts.flowStartObserved
734
+ flowStartObserved: fallbackCounts.flowStartObserved,
735
+ ...reasonContext
718
736
  };
719
737
  }
720
738
  const counts = validateTerminalCountPayload({
@@ -725,19 +743,25 @@ export function terminalMetricContextFromArgs(args, context = {}) {
725
743
  }, process.env);
726
744
  if (!counts) return {};
727
745
  return {
746
+ rootIntent: counts.rootIntent,
728
747
  userInputCountTotal: counts.total,
729
748
  userInputCountAfterFlowStart: counts.afterFlowStart,
730
- flowStartObserved: counts.flowStartObserved
749
+ flowStartObserved: counts.flowStartObserved,
750
+ ...reasonContext
731
751
  };
732
752
  }
733
753
 
734
754
  function contextFromCliArgs(args) {
735
- return {
755
+ const context = {
736
756
  runId: args['run-id'],
737
757
  rootIntent: args['root-intent'],
738
758
  productType: args['product-type'],
739
- productName: args['product-name'],
740
- ...terminalMetricContextFromArgs(args)
759
+ productName: args['product-name']
760
+ };
761
+ if (args._[0] !== 'run.terminal') return context;
762
+ return {
763
+ ...context,
764
+ ...terminalMetricContextFromArgs(args, context)
741
765
  };
742
766
  }
743
767
 
@@ -1386,22 +1410,6 @@ function readUploadStatus(env = process.env) {
1386
1410
  }
1387
1411
  }
1388
1412
 
1389
- function readPermissionedFlushState(env = process.env, nowMs = Date.now()) {
1390
- try {
1391
- const parsed = JSON.parse(fs.readFileSync(permissionedFlushStatePath(env), 'utf8'));
1392
- if (!parsed || parsed.schema_ver !== 1) return null;
1393
- const requestedAt = Number(parsed.requested_at || 0);
1394
- if (!Number.isFinite(requestedAt) || nowMs - requestedAt > permissionedFlushTtlMs) return null;
1395
- return parsed;
1396
- } catch {
1397
- return null;
1398
- }
1399
- }
1400
-
1401
- function writePermissionedFlushState(state, env = process.env) {
1402
- return writePrivateJson(permissionedFlushStatePath(env), { schema_ver: 1, ...state });
1403
- }
1404
-
1405
1413
  function networkUploadFailure(status) {
1406
1414
  const errorCode = safeDimension(status?.error_code || '', '', 80).toUpperCase();
1407
1415
  if (permissionedFlushNetworkErrors.has(errorCode)) return errorCode;
@@ -1415,47 +1423,25 @@ function networkUploadFailure(status) {
1415
1423
  return '';
1416
1424
  }
1417
1425
 
1418
- export function permissionedFlushDecision(env = process.env, nowMs = Date.now()) {
1426
+ export function permissionedFlushDecision(env = process.env) {
1419
1427
  const outboxCount = outboxFiles(env).length;
1420
1428
  const status = readUploadStatus(env);
1421
1429
  const errorCode = networkUploadFailure(status);
1422
- const existing = readPermissionedFlushState(env, nowMs);
1423
- const alreadyRequested = Boolean(existing);
1424
1430
  const result = {
1425
1431
  needed: false,
1426
1432
  outbox_count: outboxCount,
1427
- already_requested: alreadyRequested,
1428
1433
  error_code: errorCode || safeDimension(status?.error_code || '', '', 80),
1429
1434
  status: safeDimension(status?.status || 'unknown', 'unknown', 80)
1430
1435
  };
1431
1436
  if (!telemetryUploadEnabled(env) || outboxCount <= 0 || !errorCode) {
1432
1437
  return result;
1433
1438
  }
1434
- if (alreadyRequested) {
1435
- return {
1436
- ...result,
1437
- requested_at: existing.requested_at
1438
- };
1439
- }
1440
1439
  result.needed = true;
1441
1440
  return result;
1442
1441
  }
1443
1442
 
1444
- export function recordPermissionedFlushRequested(env = process.env, nowMs = Date.now(), decision = permissionedFlushDecision(env, nowMs)) {
1445
- if (!decision.needed) return decision;
1446
- writePermissionedFlushState({
1447
- requested_at: nowMs,
1448
- error_code: decision.error_code,
1449
- outbox_count: decision.outbox_count
1450
- }, env);
1451
- return {
1452
- ...decision,
1453
- requested_at: nowMs
1454
- };
1455
- }
1456
-
1457
- function emitPermissionedFlushNeededMarkers(env = process.env, nowMs = Date.now(), stream = process.stderr) {
1458
- const decision = permissionedFlushDecision(env, nowMs);
1443
+ function emitPermissionedFlushNeededMarkers(env = process.env, stream = process.stderr) {
1444
+ const decision = permissionedFlushDecision(env);
1459
1445
  if (!decision.needed || !stream || typeof stream.write !== 'function') return false;
1460
1446
  stream.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
1461
1447
  stream.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
@@ -1466,11 +1452,12 @@ export async function flushOutbox(env = process.env, {
1466
1452
  limit = outboxFlushLimit,
1467
1453
  targetFile = '',
1468
1454
  timeoutMs = uploadTimeoutMs,
1469
- sender = sendSyncLogReportDetailed
1455
+ sender = sendSyncLogReportDetailed,
1456
+ prioritizeTerminal = false
1470
1457
  } = {}) {
1471
1458
  const result = { attempted: 0, sent: 0, retained: 0, invalid: 0 };
1472
1459
  if (!telemetryUploadEnabled(env)) return result;
1473
- const files = outboxFiles(env, { targetFile, limit });
1460
+ const files = outboxFilesForFlush(env, { targetFile, limit, prioritizeTerminal });
1474
1461
  for (const file of files) {
1475
1462
  const claimedFile = claimOutboxFile(file);
1476
1463
  if (!claimedFile) continue;
@@ -1532,6 +1519,26 @@ export async function flushOutbox(env = process.env, {
1532
1519
  return result;
1533
1520
  }
1534
1521
 
1522
+ function outboxFilesForFlush(env = process.env, { targetFile = '', limit = 0, prioritizeTerminal = false } = {}) {
1523
+ const files = outboxFiles(env, { targetFile, limit: prioritizeTerminal ? 0 : limit });
1524
+ const ordered = prioritizeTerminal ? prioritizeTerminalOutboxFiles(files) : files;
1525
+ return limit > 0 ? ordered.slice(0, limit) : ordered;
1526
+ }
1527
+
1528
+ function prioritizeTerminalOutboxFiles(files) {
1529
+ return files
1530
+ .map((file, index) => {
1531
+ const record = readOutboxRecord(file);
1532
+ const stage = record?.event?.stage || record?.report?.item?.c2 || '';
1533
+ return { file, index, terminal: stage === 'run.terminal' };
1534
+ })
1535
+ .sort((left, right) => {
1536
+ if (left.terminal !== right.terminal) return left.terminal ? -1 : 1;
1537
+ return left.index - right.index;
1538
+ })
1539
+ .map((entry) => entry.file);
1540
+ }
1541
+
1535
1542
  function spawnOutboxFlush(env = process.env, { delayMs = 0 } = {}) {
1536
1543
  if (!telemetryUploadEnabled(env)) return false;
1537
1544
  try {
@@ -1564,6 +1571,34 @@ function spawnOutboxFlush(env = process.env, { delayMs = 0 } = {}) {
1564
1571
  }
1565
1572
  }
1566
1573
 
1574
+ function permissionedForegroundFlushLimitFromEnv(env = process.env) {
1575
+ const parsed = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_LIMIT);
1576
+ return parsed && parsed > 0 ? Math.min(parsed, outboxFlushLimit) : permissionedForegroundFlushLimit;
1577
+ }
1578
+
1579
+ export async function runPermissionedOutboxFlush(env = process.env, {
1580
+ limit = permissionedForegroundFlushLimitFromEnv(env),
1581
+ timeoutMs = uploadTimeoutMs,
1582
+ sender = sendSyncLogReportDetailed,
1583
+ spawnBackground = false
1584
+ } = {}) {
1585
+ const flush = await flushOutbox(env, {
1586
+ limit,
1587
+ timeoutMs,
1588
+ sender,
1589
+ prioritizeTerminal: true
1590
+ });
1591
+ const backgroundRetry = spawnBackground
1592
+ && flush.attempted > 0
1593
+ && outboxFiles(env).length > 0
1594
+ ? spawnOutboxFlush(env, { delayMs: terminalBackgroundRetryDelayMs })
1595
+ : false;
1596
+ return {
1597
+ flush,
1598
+ background_retry: backgroundRetry
1599
+ };
1600
+ }
1601
+
1567
1602
  function foregroundFlushTerminalOutboxFile(file, env = process.env) {
1568
1603
  if (!telemetryUploadEnabled(env) || !file) return false;
1569
1604
  const parsed = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_TERMINAL_FLUSH_TIMEOUT_MS);
@@ -1606,7 +1641,11 @@ function scheduleSyncLogUpload(event, env = process.env) {
1606
1641
  return false;
1607
1642
  }
1608
1643
  if (event?.stage === 'run.terminal') {
1609
- if (!foregroundFlushTerminalOutboxFile(queuedFile, env)) {
1644
+ if (foregroundFlushTerminalOutboxFile(queuedFile, env)) {
1645
+ if (outboxFiles(env).length > 0) {
1646
+ spawnOutboxFlush(env, { delayMs: terminalBackgroundRetryDelayMs });
1647
+ }
1648
+ } else {
1610
1649
  spawnOutboxFlush(env, { delayMs: terminalBackgroundRetryDelayMs });
1611
1650
  emitPermissionedFlushNeededMarkers(env);
1612
1651
  }
@@ -1680,7 +1719,7 @@ export async function runTelemetryCli(argv = process.argv) {
1680
1719
  }
1681
1720
  if (command === 'permissioned-flush-status' && args._.length === 0) {
1682
1721
  assertCliArgs(args, new Set(), 0);
1683
- const decision = permissionedFlushDecision(process.env, Date.now());
1722
+ const decision = permissionedFlushDecision(process.env);
1684
1723
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_STATUS=${JSON.stringify(decision)}\n`);
1685
1724
  if (decision.needed) {
1686
1725
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_CANDIDATE=true\n`);
@@ -1689,7 +1728,7 @@ export async function runTelemetryCli(argv = process.argv) {
1689
1728
  }
1690
1729
  if (command === 'permissioned-flush-needed' && args._.length === 0) {
1691
1730
  assertCliArgs(args, new Set(), 0);
1692
- const decision = permissionedFlushDecision(process.env, Date.now());
1731
+ const decision = permissionedFlushDecision(process.env);
1693
1732
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
1694
1733
  if (decision.needed) process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
1695
1734
  return 0;
@@ -1697,10 +1736,7 @@ export async function runTelemetryCli(argv = process.argv) {
1697
1736
  if (command === 'flush-outbox') {
1698
1737
  assertCliArgs(args, new Set(), 0);
1699
1738
  if (!telemetryEnabled(process.env)) return 0;
1700
- const decision = permissionedFlushDecision(process.env, Date.now());
1701
- if (spawnOutboxFlush(process.env)) {
1702
- recordPermissionedFlushRequested(process.env, Date.now(), decision);
1703
- }
1739
+ await runPermissionedOutboxFlush(process.env, { spawnBackground: true });
1704
1740
  return 0;
1705
1741
  }
1706
1742
  if (command === 'flush-outbox-worker') {
@@ -1725,6 +1761,7 @@ export async function runTelemetryCli(argv = process.argv) {
1725
1761
  'root-intent',
1726
1762
  'product-type',
1727
1763
  'product-name',
1764
+ 'terminal-reason',
1728
1765
  'timestamp-ms',
1729
1766
  'user-input-count-total',
1730
1767
  'user-input-count-after-flow-start',
@@ -1,4 +1,4 @@
1
- - 字段白名单:stage/flow、产品、根意图、时间戳、Skill 版本、宿主/模型、匿名机器哈希、`run_id`、终态计数;禁密钥/账号密码/订单/交易号/截图/凭证/用户输入/路径/URL/响应/命令输出/堆栈原文;失败不影响业务,内部行 `ALIPAY_AIPAY_INTERNAL:`。
1
+ - 字段白名单:stage/flow、产品、根意图、时间戳、Skill 版本、宿主/模型、匿名机器哈希、`run_id`、终态计数和终态原因枚举;禁密钥/账号密码/订单/交易号/截图/凭证/用户输入/路径/URL/响应/命令输出/堆栈原文;失败不影响业务,内部行 `ALIPAY_AIPAY_INTERNAL:`。
2
2
  - run:`start-run` 创建/复用 `run_id`,脚本继承 `PLATFORM_ID=<run_id>`;`root_intent + product_type` 判定复用/新建,`UNKNOWN` 补全产品仍复用同一 `run_id`;换产品/根意图/已终态/重新开始/重新走流程/作废本轮流程才新建。`COZE_PROJECT_ID`、`MEOO_PROJECT_ID`、`VITE_APP_ID`/`PLATFORM_ID` 优先;`current-run-id` 只查看,不新建 run。outbox 2xx 才删;维护/测试/Benchmark 禁上传。
3
- - 计数:统计窗口从首次触发本 Skill 的用户消息开始,到当前 `run_id` 的 `run.terminal` 事件生成为止。只计用户在 Agent 对话框发送的消息,不计宿主权限弹窗、系统授权、网页授权、扫码、Skill stage、runner、工具调用或自动轮询;文字、图片/文件上传、空白或短确认消息、材料补充、确认回复和中途插话都按一条用户消息计,同一条消息同时包含文字和图片/文件时仍只计一条。`USER_INPUT_ID` 由 Agent 从首次触发本 Skill 的用户消息开始,在当前对话内给每条新用户消息分配单调序号,同一消息重试复用同一值;禁止包含或派生自用户输入内容、文件名、路径、截图/OCR、业务材料、stage 名或工具调用序号,运行时只保存本地 hash 且不上报。`start-run --input-id "$USER_INPUT_ID"` 自动把触发当前 run 的首条用户输入计为 `before-flow`;若中断/压缩后用新的 `USER_INPUT_ID` 重复进入同一未终态 run,`start-run` 会按当前 phase 幂等补计该用户消息。`reply confirm --reply "$USER_REPLY" --input-id "$USER_INPUT_ID"` 和 onboarding recovery 自动按当前 phase 幂等计数,禁止再为同一条回复手动执行 `count-user-input`。其他用户输入到达当前 run 后,包括用户中途打断、补充材料、只上传图片/文件或只问问题,在继续任何 Skill 动作或对该输入作业务答复前立刻执行 `telemetry count-user-input --phase before-flow|after-flow --input-id "$USER_INPUT_ID"` 写本地 run state,不联网;重复同一 `USER_INPUT_ID` 不加计。`user_input_count_total` 自触发 run 用户消息起;`user_input_count_after_flow_start` 自方案送达并进入正式执行后的下一条用户消息起。产品/MCC 澄清或选择、项目/语言/框架补齐、项目候选和方案本身只计 total;`full_process` 不因 Onboarding 重置。`flow_start_observed`=正式 flow start,由 `integration.user_confirmed|onboarding.user_confirmed` 自动标记但不计数;flow start 前拒绝 `after-flow`,用户 kill 未终态不补报。closeout 默认派生本地计数;状态为空时终态使用首条输入的最小合法计数,状态不可用且已核对时才显式传三项;runner 拒绝缺项、负数、`after > total`、已观测 flow start 但 `after >= total`。终态只追加计数,不新增确认点、stage 或独立上报。`run.terminal` 入队后以短超时前台 drain,仅当前 terminal 文件,避免流程最后的 detached worker 被宿主回收;若文件仍在且是网络类错误,短重试一次并挂延迟后台补传,同时输出内部 `PERMISSIONED_FLUSH_NEEDED=true` 供 Agent 当场申请补传权限;失败保留 outbox。`c4=ext_info`,计数合法且有 `root_intent` 才写 JSON。
4
- - 补传:不为权限探测新增网络事件;实际低敏上传就是自然探测,宿主已允许访问 collector 时不额外申请权限。任一终态后若 `run.terminal`/outbox 因 DNS、连接或超时失败积压,执行以下本地判断。`upload-status`/`permissioned-flush-status`/`permissioned-flush-needed` 都不联网;前两者不消耗请求窗口,`permissioned-flush-needed` 也不写 `requested_at`,避免 Agent 中断或宿主拒绝权限时误抑制后续补传。最近 `ENOTFOUND|EAI_AGAIN|ETIMEDOUT` 等 DNS/连接/超时类错误、outbox 积压、12h 未实际触发补传时,`permissioned-flush-status` 出 `PERMISSIONED_FLUSH_CANDIDATE=true`,`permissioned-flush-needed` 出唯一 `PERMISSIONED_FLUSH_NEEDED=true` 后才申请。`flush-outbox` 后台 worker 成功触发后才记录本次已发起补传,随后 POST 到 `collect.alipay.com:443`,路径 `/yuyan/`。说明:`是否允许 Agent 上报 Skill 使用过程的统计信息?只含产品类别、时间、Skill 版本、匿名设备标识、人机交互次数等用于帮助改善用户体验的非敏感信息,不会采集包含源代码、用户输入内容等任何敏感信息。` 同一 Agent 会话只对已实际触发的补传抑制重复请求;无宿主标识时按当前 `run_id` 隔离 12h;拒绝/中断且 `flush-outbox` 未执行时不写抑制状态,本地 worker 触发失败也不写抑制状态;执行后失败/非2xx留 outbox;collector 结果非业务条件;无 `inspect-outbox`、`--confirm-reviewed`、`alipay-cli whoami` 兜底。
3
+ - 计数:统计窗口从首次触发本 Skill 的用户消息开始,到当前 `run_id` 的 `run.terminal` 事件生成为止。只计用户在 Agent 对话框发送的消息,不计宿主权限弹窗、系统授权、网页授权、扫码、Skill stage、runner、工具调用或自动轮询;文字、图片/文件上传、空白或短确认消息、材料补充、确认回复和中途插话都按一条用户消息计,同一条消息同时包含文字和图片/文件时仍只计一条。`USER_INPUT_ID` 由 Agent 从首次触发本 Skill 的用户消息开始,在当前对话内给每条新用户消息分配单调序号,同一消息重试复用同一值;禁止包含或派生自用户输入内容、文件名、路径、截图/OCR、业务材料、stage 名或工具调用序号,运行时只保存本地 hash 且不上报。`start-run --input-id "$USER_INPUT_ID"` 自动把触发当前 run 的首条用户输入计为 `before-flow`;若中断/压缩后用新的 `USER_INPUT_ID` 重复进入同一未终态 run,`start-run` 会按当前 phase 幂等补计该用户消息。`reply confirm --reply "$USER_REPLY" --input-id "$USER_INPUT_ID"` 和 onboarding recovery 自动按当前 phase 幂等计数,禁止再为同一条回复手动执行 `count-user-input`。其他用户输入到达当前 run 后,包括用户中途打断、补充材料、只上传图片/文件或只问问题,在继续任何 Skill 动作或对该输入作业务答复前立刻执行 `telemetry count-user-input --phase before-flow|after-flow --input-id "$USER_INPUT_ID"` 写本地 run state,不联网;重复同一 `USER_INPUT_ID` 不加计。`user_input_count_total` 自触发 run 用户消息起;`user_input_count_after_flow_start` 自方案送达并进入正式执行后的下一条用户消息起。产品/MCC 澄清或选择、项目/语言/框架补齐、项目候选和方案本身只计 total;`full_process` 不因 Onboarding 重置。`flow_start_observed`=正式 flow start,由 `integration.user_confirmed|onboarding.user_confirmed` 自动标记但不计数;flow start 前拒绝 `after-flow`,用户 kill 未终态不补报。closeout 默认派生本地计数;状态为空时终态使用首条输入的最小合法计数,状态不可用且已核对时才显式传三项;runner 拒绝缺项、负数、`after > total`、已观测 flow start 但 `after >= total`。`terminal_reason` 只允许 `completed|partial_with_remaining_actions|integration_blocked|unsupported_product|unsupported_platform|enterprise_account_unsupported|unknown`,由终态 runner 依据机器事实派生,禁止自由文本原因。终态只追加计数和原因枚举,不新增确认点、stage 或独立上报。`run.terminal` 入队后以短超时前台 drain,仅当前 terminal 文件,避免流程最后的 detached worker 被宿主回收;当前 terminal 文件成功发送但仍有历史 outbox 积压时挂延迟后台补传;若文件仍在且是网络类错误,短重试一次并挂延迟后台补传,同时输出内部 `PERMISSIONED_FLUSH_NEEDED=true` 供 Agent 当场进入通用补传路径;失败保留 outbox。`c4=ext_info`,计数合法且有 `root_intent` 才写 JSON。
4
+ - 补传:不为权限探测新增网络事件;实际低敏上传就是自然探测,宿主已允许访问 collector 时不额外申请权限。任一 telemetry 上传若因 DNS、连接或超时失败导致 outbox 积压,均使用同一套本地判断和补传机制。`upload-status`/`permissioned-flush-status`/`permissioned-flush-needed` 都不联网、不写请求窗口或抑制状态;只要最近错误是 `ENOTFOUND|EAI_AGAIN|ETIMEDOUT` 等 DNS/连接/超时类错误且 outbox 仍有积压,`permissioned-flush-status` 出 `PERMISSIONED_FLUSH_CANDIDATE=true`,`permissioned-flush-needed` 出唯一 `PERMISSIONED_FLUSH_NEEDED=true` 后才按需申请补传权限。`flush-outbox` 前台完成一次有界小批量补传,优先发送积压的 `run.terminal`,再处理其他 outbox;失败、非 2xx 或仍有剩余积压时继续保留 outbox,失败后仍按实时条件继续允许补传,并可挂一个延迟后台 worker 作兜底。目标限定为 POST `collect.alipay.com:443` 的 `/yuyan/` 路径。说明:`是否允许 Agent 上报 Skill 使用过程的统计信息?只含产品类别、时间、Skill 版本、匿名设备标识、人机交互次数等用于帮助改善用户体验的非敏感信息,不会采集包含源代码、用户输入内容等任何敏感信息。` collector 结果非业务条件;无 `inspect-outbox`、`--confirm-reviewed`、`alipay-cli whoami` 兜底。
@@ -48,11 +48,11 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
48
48
 
49
49
  `full_process` 识别根意图后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry start-run --root-intent full_process --input-id "$USER_INPUT_ID"`;产品已唯一确定时追加 `--product-type <aipay|webpay|apppay>`。`integration_only` 和 `onboarding_only` 已由上表各自 bootstrap 包含同一次 `start-run`,不得重复执行本条 telemetry。命令创建/复用当前流程 `run_id`,并在遥测开启时记录 `run.intent_detected`:同一意图和产品继续复用,`UNKNOWN` 补全产品仍复用同一 `run_id`;明确换产品、换根意图或既有 run 已终态才重建。第三方平台项目 ID 仍优先固定为 `run_id`,不得被显式 run ID 或本地生成值覆盖。业务 stage 只由登记 runner/script 异步记录。
50
50
 
51
- 遥测默认开启,`ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 关闭事件/outbox/上传/flush;私有 run context 仍供 `PLATFORM_ID` 与终态生命周期。失败静默,不改变业务/消息/确认,stdout 空且 stderr 内部行不对客。`integration_only` 由 checklist 终结;Onboarding 由 closeout 或登记提前终止消息终结。上报权限不新增网络探针;实际低敏上传就是自然探测,collector 可达时不申请。任一终态后若 `run.terminal`/outbox 因 DNS、连接或超时失败积压,按 `normal/telemetry.md` 先本地状态判断,唯一需要时才申请一次补传权限。
51
+ 遥测默认开启,`ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 关闭事件/outbox/上传/flush;私有 run context 仍供 `PLATFORM_ID` 与终态生命周期。失败静默,不改变业务/消息/确认,stdout 空且 stderr 内部行不对客。`integration_only` 由 checklist 终结;Onboarding 由 closeout 或登记提前终止消息终结。上报权限不新增网络探针;实际低敏上传就是自然探测,collector 可达时不申请。任一终态后若 telemetry outbox 因 DNS、连接或超时失败积压,按 `normal/telemetry.md` 先本地状态判断,确实需要时按需申请补传权限;`flush-outbox` 前台小批量补传并优先发送 `run.terminal`,失败仍不影响业务收口。
52
52
 
53
- 用户输入计数是入口前置职责,统计窗口从首次触发本 Skill 的用户消息开始,到当前 `run_id` 的 `run.terminal` 事件生成为止。只计用户在 Agent 对话框发送的消息,不计宿主/系统/网页授权、扫码、Skill stage、runner、工具调用或自动轮询。文字、图片/文件上传、空白或短确认消息、材料补充、确认回复和中途插话都按一条用户消息计数;同一条消息同时包含文字和图片/文件时仍只计一条。`USER_INPUT_ID` 是当前用户消息的本地幂等键:Agent 从首次触发本 Skill 的用户消息开始,在当前对话内给每条新用户消息分配单调序号,并在同一消息的重试中复用同一值。`USER_INPUT_ID` 不得包含或派生自用户输入内容、文件名、路径、截图/OCR、业务材料、stage 名或工具调用序号;运行时只保存其本地 hash,不上报。
53
+ 用户输入计数是入口前置职责,统计窗口从首次触发本 Skill 的用户消息开始,到当前 `run_id` 的 `run.terminal` 事件生成为止。只计用户在 Agent 对话框发送的消息,不计宿主/系统/网页授权、扫码、Skill stage、runner、工具调用或自动轮询。文字、图片/文件、空白/短确认、材料补充、确认回复和中途插话都按一条用户消息计;同一条消息含文字和图片/文件仍只计一条。`USER_INPUT_ID` 是当前用户消息的本地幂等键:Agent 从首次触发本 Skill 起,给每条新用户消息分配对话内单调序号,同一消息重试复用同一值。`USER_INPUT_ID` 不得包含或派生自用户输入内容、文件名、路径、截图/OCR、业务材料、stage 名或工具调用序号;运行时只保存其本地 hash,不上报。
54
54
 
55
- `start-run` 自动把触发当前 run 的首条用户输入计为 `before-flow`;若中断/压缩后用新的 `USER_INPUT_ID` 重复进入同一未终态 run,`start-run` 会按当前 phase 幂等补计该用户消息。阻塞确认统一通过 `reply confirm --reply "$USER_REPLY" --input-id "$USER_INPUT_ID"`,该 runner 自动按当前 phase 幂等计数,禁止再为同一条确认回复额外执行 `telemetry count-user-input`。其他不会经过 `reply confirm` 或 onboarding recovery 的用户输入,包括用户中途打断、补充材料、只上传图片/文件或只问问题,在继续任何 Skill 动作或对该输入作业务答复前按当前 phase 执行一次 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry count-user-input --phase before-flow|after-flow --input-id "$USER_INPUT_ID"`;重复同一 `USER_INPUT_ID` 不加计。`*.user_confirmed` 由 flow loader 自动标记起点但不计数,after-flow 从其后的下一条用户消息开始,前置澄清和候选不计 after-flow,`full_process` 不因 Onboarding 重置。closeout 默认从本地 run state 派生计数;状态为空时终态使用首条输入的最小合法计数,状态不可用且已核对时才显式传三项计数参数,runner 拒绝缺项、负数、`after > total`、已观测 flow start 但 `after >= total` 等明显错误。不新增业务确认点或独立上报;入队后当前 `run.terminal` 文件做短超时前台 drain,失败仍保留 outbox 且不影响业务收口,网络类失败按维护文档短重试/后台兜底。脚本以当前 `run_id` 为 `PLATFORM_ID`,Agent 不手写环境前缀或改 MCP payload;维护诊断、outbox、上传和 flush 细节只在 `normal/telemetry.md` 按需读取。
55
+ `start-run` 自动把触发当前 run 的首条用户输入计为 `before-flow`;中断/压缩后以新 `USER_INPUT_ID` 重入同一未终态 run 时,`start-run` 按当前 phase 幂等补计该消息。阻塞确认统一通过 `reply confirm --reply "$USER_REPLY" --input-id "$USER_INPUT_ID"`,该 runner 自动按当前 phase 幂等计数,禁止再为同一确认回复额外执行 `telemetry count-user-input`。其他用户输入(中途打断、补材料、只上传图片/文件、只问问题等)若不经 `reply confirm` 或 onboarding recovery,先于任何 Skill 动作或业务答复按当前 phase 执行一次 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry count-user-input --phase before-flow|after-flow --input-id "$USER_INPUT_ID"`;重复同一 `USER_INPUT_ID` 不加计。`*.user_confirmed` 由 flow loader 自动标记起点但不计数,after-flow 从其后的下一条用户消息开始,前置澄清和候选不计 after-flow,`full_process` 不因 Onboarding 重置。closeout 默认从本地 run state 派生计数;状态为空时用首条输入的最小合法计数,状态不可用且已核对时才显式传三项,runner 拒绝缺项、负数、`after > total`、已观测 flow start 但 `after >= total`。`terminal_reason` 为终态 runner 写入的固定低敏枚举,禁自由文本、错误详情和业务响应。不新增业务确认点或独立上报;入队后当前 `run.terminal` 文件做短超时前台 drain,失败保留 outbox 且不影响业务收口,网络类失败进入通用补传判断。脚本以当前 `run_id` 为 `PLATFORM_ID`;Agent 不手写环境前缀或改 MCP payload;维护诊断/outbox/上传/flush 见 `normal/telemetry.md`。
56
56
 
57
57
  三种入口均只在无法从已有产品名称或业务描述可靠确定唯一产品时执行 flow 登记的 `product-clarify` runner;不得推定或手写澄清。产品明确但 MCC 尚无检索输入时使用 `mcc-clarify`;已有经营描述后先按 Onboarding flow 读取一次当前 `<SKILL_DIR>/references/onboarding/modules/mcc-reference.md` 做 Agent 语义候选,再统一调用 `mcc-resolve-plan` 由 runner 复核、合并和渲染;用户直接提供当前表内完整 MCC 编码时可跳过 Agent 泛化并由 runner 精确校验。不得拆回 `mcc-search + mcc-candidates`、先旧检索失败后临时猜测,或为方案展示再拼 renderer 输入。
58
58