@alipay/alipay-aipay 1.6.2-beta2 → 1.6.2

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-beta2",
3
+ "version": "1.6.2",
4
4
  "description": "支付宝 AI 付 Skill 安装包 - 优先安装 Agent Skill,并在 macOS/Linux 后台准备用户级 CLI 工具",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -5,23 +5,27 @@ description: >-
5
5
  触发关键词:"AI支付"、"接支付宝"、"一站式接支付宝"、"接个支付"、"开发支付"、"402收款"、"AI按量付费"、"AI网页应用收款"、"AI移动应用收款"、"按量付费"、"快捷收款"、"快捷收单"、"网站支付"、"APP 支付"、"产品开通"、"商家开通"、"签约入驻"等。
6
6
  ---
7
7
 
8
- 本文件只负责在加载业务规则前确定本轮实际使用的 Skill 版本。禁止在完成下列启动协议前读取 `references/runtime-entry.md`、flow、模块、消息目录或业务脚本。
8
+ # 支付宝 AI 付 Skill 启动器
9
9
 
10
- 首次使用 `alipay-aipay`:解析本文件目录为规范化绝对 `<SKILL_DIR>`,记录 `ALIPAY_AIPAY_SELF_UPDATE_CHECKED=true`,执行单命令;`<SKILL_DIR>` 是占位符,禁止用 shell 变量赋值、命令替换或依赖特定 shell:
10
+ 本文件只负责在加载业务规则前确定本轮实际使用的 Skill 版本。禁止在完成下列启动协议前读取 `references/runtime-entry.md`、任一 flow、模块、消息目录或业务脚本。
11
+
12
+ ## 前置版本检查
13
+
14
+ 本次 Agent 对话首次使用 `alipay-aipay` 时,先把本文件所在目录解析为规范化绝对路径 `<SKILL_DIR>`,记录一次性事实 `ALIPAY_AIPAY_SELF_UPDATE_CHECKED=true`,再执行以下单条命令。`<SKILL_DIR>` 是执行时替换的路径占位符;禁止用 shell 变量赋值、命令替换或依赖特定 shell:
11
15
 
12
16
  ```text
13
17
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" self-update check --skill-dir "<SKILL_DIR>"
14
18
  ```
15
19
 
16
- `SKILL_DIR` 只绑定当前 Skill,禁止换成 Agent 启动目录、用户项目、Home、npm 源码根或其他副本。`VERSION` 缺失/非法或入口缺失由命令修复;失败或 marker 不唯一按 `SELF_UPDATE:CHECK_FAILED`。
20
+ `SKILL_DIR` 必须绑定刚触发且正在执行本命令的 Skill 规范化绝对路径,不得换成 Agent 启动目录、用户项目目录、用户 Home、npm 包源码根或另一份 Skill 副本。`VERSION` 缺失/非法或 `references/runtime-entry.md` 缺失时由本命令尝试精确修复;命令失败或 marker 无法唯一解析时按 `SELF_UPDATE:CHECK_FAILED`。
17
21
 
18
- stderr 只允许一个内部终态,禁止展示 marker 或 npm/npx 原始输出:
22
+ stderr 只允许出现以下一个内部终态;不得向用户展示 marker 或 npm/npx 原始输出:
19
23
 
20
- - `SELF_UPDATE:UPDATED=<合法 SemVer>`:已切换新版。
24
+ - `SELF_UPDATE:UPDATED=<合法 SemVer>`:当前 Skill 目录已原子切换到新版。
21
25
  - `SELF_UPDATE:CURRENT=<合法 SemVer>`:继续当前版本。
22
- - `SELF_UPDATE:RETRY_WITH_NETWORK`:可取权后原样重试一次。
23
- - `SELF_UPDATE:SKIPPED_NETWORK_RESTRICTED|CHECK_FAILED|INSTALL_FAILED|VERIFY_FAILED`:继续当前可用版本。
26
+ - `SELF_UPDATE:RETRY_WITH_NETWORK`:registry 或安装阶段网络不可达;可申请联网后原样重试本命令一次。
27
+ - `SELF_UPDATE:SKIPPED_NETWORK_RESTRICTED|CHECK_FAILED|INSTALL_FAILED|VERIFY_FAILED`:静默继续当前可用版本,不重试、不申请权限。
24
28
 
25
- 除 `RETRY_WITH_NETWORK` 外,只完整读取同一 `SKILL_DIR` 的 `references/runtime-entry.md`,记录 `ALIPAY_AIPAY_RUNTIME_ENTRY_LOADED=true` 和路径,再路由。`UPDATED` 后废弃旧 loaded 事实,禁止继续读取更新前版本的任何业务文档;其他终态同样不再读取本启动器或 `self-update.md` 补充规则。
29
+ 除 `RETRY_WITH_NETWORK` 的一次原样重试外,只完整读取同一 `SKILL_DIR` 的 `references/runtime-entry.md`,在当前上下文记录 `ALIPAY_AIPAY_RUNTIME_ENTRY_LOADED=true` 和规范化路径,再按原请求路由。`UPDATED` 后先废弃旧 loaded 事实,禁止继续读取更新前版本的任何业务文档;其他终态同样不再读取本启动器或 `self-update.md` 补充规则。
26
30
 
27
- 同一对话后续确认、补材料、候选、恢复、中断继续、重复触发和 Full Process 衔接,两项事实存在且路径未变时跳过版本检查和入口重读;仅 `UPDATED`、路径变化或上下文压缩导致规则不可用时重读并刷新;事实不落盘。
31
+ 同一对话后续确认、补材料、候选选择、恢复、中断继续、重复触发和 Full Process 衔接,两项事实存在、路径未变且入口规则仍在时,跳过版本检查和入口重读。仅 `UPDATED`、路径变化或上下文压缩导致规则不可用时重读并刷新;事实不落盘。
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 1.6.2-beta2
1
+ 1.6.2
@@ -203,7 +203,7 @@ runner 失败时不得展示手写方案或进入步骤 3。成功时必须取
203
203
 
204
204
  - `full_process`:`../normal/full-process-routing.md` 已使用同一个 `integration.start.confirm`,在同一方案中绑定检查器产生的项目来源、代码状态和其他产品,以及产品、项目路径、语言/框架和服务声明;消息送达后已经直接进入本步骤,禁止再次渲染或要求确认。该消息不展示步骤清单,也不缩减执行范围;后续仍必须按当前产品完成本 flow 的全部适用接口、完成条件和 checklist。用户中断后若项目、产品、语言或框架发生变化,返回完整接入 router 重新澄清和扫描,再输出一条当前方案并继续。该方案仍禁止提前展示 MCC、授权范围、产品开通材料或 onboarding 待办。
205
205
 
206
- 方案送达后只通过上述 development loader 进入步骤 3;loader 仍在原位置记录兼容 stage `integration.user_confirmed` 和 `integration.pre_dev_reading.start`。前者表示已基于完整确定事实自动进入执行,不表示收到额外确认回复;不得另行执行 telemetry 命令或伪造用户回复。after-flow 从此后下一条用户消息开始;前置澄清、候选和方案只计 total。
206
+ 方案送达后只通过上述 development loader 进入步骤 3;loader 仍在原位置记录兼容 stage `integration.user_confirmed` 和 `integration.pre_dev_reading.start`。前者表示已基于完整确定事实自动进入执行并自动标记计数 flow start,不表示收到额外确认回复;不得另行执行 telemetry 命令或伪造用户回复。after-flow 从此后下一条用户消息开始;前置澄清、候选和方案只计 total。
207
207
 
208
208
  **方案后的上下文锁定**:已有项目保持步骤 1 前已经唯一解析的实际根目录和原有工程结构;新项目必须已经在步骤 1 前按用户明确选择的方式完成初始化并通过构建标识检查。此处不重复准备、初始化或探测项目。路径内容与步骤 1 成功时不一致则停止并重新澄清;只有目标项目或服务端语言变化时返回步骤 1,其他情况直接进入步骤 3。
209
209
 
@@ -554,20 +554,22 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message saf
554
554
 
555
555
  <!-- ALIPAY_AIPAY_PRODUCT:aipay:START -->
556
556
  ```bash
557
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message checklist-result --variant AIPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --sandbox-result "$SANDBOX_RESULT" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER" --user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>
557
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message checklist-result --variant AIPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --sandbox-result "$SANDBOX_RESULT" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER"
558
558
  ```
559
559
  <!-- ALIPAY_AIPAY_PRODUCT:END -->
560
560
  <!-- ALIPAY_AIPAY_PRODUCT:webpay:START -->
561
561
  ```bash
562
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message checklist-result --variant WEBPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --webpay-experience-result "$WEBPAY_EXPERIENCE_RESULT" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER" --user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>
562
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message checklist-result --variant WEBPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --webpay-experience-result "$WEBPAY_EXPERIENCE_RESULT" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER"
563
563
  ```
564
564
  <!-- ALIPAY_AIPAY_PRODUCT:END -->
565
565
  <!-- ALIPAY_AIPAY_PRODUCT:apppay:START -->
566
566
  ```bash
567
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message checklist-result --variant APPPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER" --user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>
567
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-message checklist-result --variant APPPAY --project-path "$PROJECT_PATH" --scope-receipt "$CHECKLIST_SCOPE_RECEIPT" --overall-result "$OVERALL_RESULT" --failed-items "$FAILED_ITEMS" --manual-items "$MANUAL_ITEMS" --sandbox-config-state "$SANDBOX_CONFIG_STATE" --blocking-defect-state "$BLOCKING_DEFECT_STATE" --execution-mode "$EXECUTION_MODE" --next-flow-reminder "$NEXT_FLOW_REMINDER"
568
568
  ```
569
569
  <!-- ALIPAY_AIPAY_PRODUCT:END -->
570
570
 
571
+ 用户输入计数默认由 telemetry run state 派生。只有本地计数状态不可用且已重新核对时,才在 checklist 命令末尾同时追加 `--user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>`;runner 会拒绝明显不合理计数。
572
+
571
573
  同一次代码开发流程只渲染一次最终 checklist 结果;renderer stdout 是本次默认收口的唯一对客正文,禁止在前后追加风险说明、通过项摘要、执行说明、衔接提醒或第二份待办。
572
574
 
573
575
  这些产品专属字段只用于 renderer 校验最终结论,不在 checklist 对客结果中重复展示。APP 支付未来沙箱测试对客引导的维护 TODO 只保留在消息目录对应变体,不得渲染给用户;补充时必须同步本 flow、`modules/checklist.md` 和测试。
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
7
7
 
8
8
  import { checklistForProduct } from './checklist_sections.mjs';
9
9
  import { createChecklistScopeReceipt } from './integration_message_runner.mjs';
10
- import { emitStageMetricAsync } from './telemetry.mjs';
10
+ import { emitStageMetricAsync, markTelemetryFlowStart } from './telemetry.mjs';
11
11
 
12
12
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
13
13
  const referencesDir = path.resolve(scriptDir, '../..');
@@ -239,6 +239,9 @@ export function runFlowCli(argv = process.argv) {
239
239
  const phaseBlock = extractFlowBlock(source, phaseBlockName(flow, phase));
240
240
  for (const stage of stageDeclarations(phaseBlock, productType)) {
241
241
  emitStageMetricAsync(stage, { productType });
242
+ if (stage === 'integration.user_confirmed' || stage === 'onboarding.user_confirmed') {
243
+ markTelemetryFlowStart();
244
+ }
242
245
  }
243
246
  process.stdout.write(`${loadFlowBundle(flow, phase, { productType })}\n`);
244
247
  }
@@ -1357,7 +1357,11 @@ async function runChecklistResult(args) {
1357
1357
  }
1358
1358
  const rendered = renderMessage('integration.checklist.result', variant, input);
1359
1359
  if (input.executionMode === '仅代码开发') {
1360
- emitTerminalMetric({ rootIntent: 'integration_only', productType: variant, ...terminalMetricContextFromArgs(args) });
1360
+ emitTerminalMetric({
1361
+ rootIntent: 'integration_only',
1362
+ productType: variant,
1363
+ ...terminalMetricContextFromArgs(args, { rootIntent: 'integration_only' })
1364
+ });
1361
1365
  }
1362
1366
  return rendered;
1363
1367
  }
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { fileURLToPath } from 'node:url';
7
+ import { recordTelemetryUserInputForCurrentPhase } from './telemetry.mjs';
7
8
 
8
9
  const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
9
10
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
@@ -113,11 +114,18 @@ function parseArgs(argv) {
113
114
  return { command, args };
114
115
  }
115
116
 
117
+ function required(args, name) {
118
+ const value = args[name];
119
+ if (typeof value !== 'string' || !value) throw new Error(`缺少参数 --${name}`);
120
+ return value;
121
+ }
122
+
116
123
  export function runInteractionReplyCli(argv = process.argv) {
117
124
  const { command, args } = parseArgs(argv);
118
125
  if (command !== 'confirm' || typeof args.reply !== 'string' || !args.reply) {
119
- throw new Error('用法: interaction_reply.mjs confirm --reply <用户原始回复>');
126
+ throw new Error('用法: interaction_reply.mjs confirm --reply <用户原始回复> --input-id <本地用户输入幂等键>');
120
127
  }
128
+ recordTelemetryUserInputForCurrentPhase(required(args, 'input-id'));
121
129
  const decision = isAffirmativeReply(args.reply) ? 'CONFIRMED' : 'REJECTED';
122
130
  process.stderr.write(`${internalPrefix}REPLY_DECISION=${decision}\n`);
123
131
  if (decision === 'REJECTED') process.exitCode = 2;
@@ -751,7 +751,11 @@ function closeoutInputFromArgs(args) {
751
751
  async function runCloseout(args) {
752
752
  const variables = closeoutInputFromArgs(args);
753
753
  const rendered = renderMessage('process.partial_result', 'DEFAULT', variables);
754
- emitTerminalMetric({ productName: variables.productName, ...terminalMetricContextFromArgs(args) });
754
+ emitTerminalMetric({
755
+ productName: variables.productName,
756
+ rootIntent: args['root-intent'],
757
+ ...terminalMetricContextFromArgs(args, { rootIntent: args['root-intent'] })
758
+ });
755
759
  return rendered;
756
760
  }
757
761
 
@@ -5,6 +5,7 @@ import path from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { isAffirmativeReply } from './interaction_reply.mjs';
8
+ import { recordTelemetryUserInputForCurrentPhase } from './telemetry.mjs';
8
9
 
9
10
  const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
10
11
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
@@ -33,6 +34,7 @@ function required(args, name) {
33
34
  }
34
35
 
35
36
  function assertRegisteredReply(args, kind) {
37
+ recordTelemetryUserInputForCurrentPhase(required(args, 'input-id'));
36
38
  if (!isAffirmativeReply(required(args, 'reply'))) {
37
39
  process.stderr.write(`${internalPrefix}RECOVERY_INPUT_REJECTED=${kind}\n`);
38
40
  process.exitCode = 2;
@@ -85,7 +87,7 @@ export function runOnboardingRecoveryCli(argv = process.argv) {
85
87
  const { command, args } = parseArgs(argv);
86
88
  if (command === 'auth-confirm') return runAuthConfirm(args);
87
89
  if (command === 'application-key') return runApplicationKeyRecovery(args);
88
- throw new Error('用法: onboarding_recovery_runner.mjs <auth-confirm|application-key> --reply <明确完成表达> ...');
90
+ throw new Error('用法: onboarding_recovery_runner.mjs <auth-confirm|application-key> --reply <明确完成表达> --input-id <本地用户输入幂等键> ...');
89
91
  }
90
92
 
91
93
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
@@ -132,19 +132,20 @@ function appendOptionalTerminalCountOptions(targetArgs, options) {
132
132
  }
133
133
  }
134
134
 
135
- async function startTelemetryRun(rootIntent, productType) {
135
+ async function startTelemetryRun(rootIntent, productType, inputId = '') {
136
136
  const telemetryArgs = ['start-run', '--root-intent', rootIntent];
137
137
  if (productType) telemetryArgs.push('--product-type', productType);
138
+ if (inputId) telemetryArgs.push('--input-id', inputId);
138
139
  const { runTelemetryCli } = await import('./telemetry.mjs');
139
140
  await runTelemetryCli(cliArgv(telemetryArgs));
140
141
  }
141
142
 
142
143
  async function runIntegrationBootstrap(args) {
143
144
  const options = parseRuntimeArgs(args);
144
- assertOnlyOptions(options, ['root-intent', 'product-type']);
145
+ assertOnlyOptions(options, ['root-intent', 'product-type', 'input-id']);
145
146
  const rootIntent = assertEnumOption(options, 'root-intent', ['integration_only']);
146
147
  const productType = assertEnumOption(options, 'product-type', ['aipay', 'webpay', 'apppay'], { required: false });
147
- await startTelemetryRun(rootIntent, productType);
148
+ await startTelemetryRun(rootIntent, productType, options['input-id']);
148
149
  const { runFlowCli } = await import('./flow_loader.mjs');
149
150
  runFlowCli(cliArgv(['integration', 'entry']));
150
151
  }
@@ -154,13 +155,14 @@ async function runOnboardingBootstrap(args) {
154
155
  assertOnlyOptions(options, [
155
156
  'root-intent',
156
157
  'product-type',
158
+ 'input-id',
157
159
  'user-input-count-total',
158
160
  'user-input-count-after-flow-start',
159
161
  'flow-start-observed'
160
162
  ]);
161
163
  const rootIntent = assertEnumOption(options, 'root-intent', ['onboarding_only']);
162
164
  const productType = assertEnumOption(options, 'product-type', ['aipay', 'webpay', 'apppay'], { required: false });
163
- await startTelemetryRun(rootIntent, productType);
165
+ await startTelemetryRun(rootIntent, productType, options['input-id']);
164
166
  const { runOnboardingMessageCli } = await import('./onboarding_message_runner.mjs');
165
167
  const platformEntryArgs = ['platform-entry'];
166
168
  appendOptionalTerminalCountOptions(platformEntryArgs, options);
@@ -19,12 +19,35 @@ const syncLogMsg = 'alipay_aipay_skill_event';
19
19
  const syncLogYuyanID = '180020010001290755';
20
20
  const uploadTimeoutMs = 2500;
21
21
  const terminalForegroundUploadTimeoutMs = 1200;
22
+ const terminalForegroundUploadMaxAttempts = 2;
23
+ const terminalForegroundUploadRetryDelayMs = 300;
24
+ const terminalBackgroundRetryDelayMs = 1000;
25
+ const terminalCountArgNames = Object.freeze([
26
+ 'user-input-count-total',
27
+ 'user-input-count-after-flow-start',
28
+ 'flow-start-observed'
29
+ ]);
30
+ const initialUserInputId = 'run.start';
22
31
  const outboxFlushLimit = 50;
23
32
  const outboxClaimTtlMs = 5 * 60 * 1000;
24
33
  const outboxMaxRecords = 1000;
25
34
  const outboxMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
26
35
  const permissionedFlushTtlMs = 12 * 60 * 60 * 1000;
27
36
  const platformProjectIdEnvKeys = Object.freeze(['COZE_PROJECT_ID', 'MEOO_PROJECT_ID']);
37
+ const sessionStateEnvKeys = Object.freeze([
38
+ 'CODEX_THREAD_ID',
39
+ 'CODEX_SESSION_ID',
40
+ 'CLAUDE_CODE_SESSION_ID',
41
+ 'OPENCLAW_SESSION_ID',
42
+ 'CURSOR_SESSION_ID',
43
+ 'CURSOR_TRACE_ID',
44
+ 'COZE_SESSION_ID',
45
+ 'COZE_CONVERSATION_ID',
46
+ 'HERMES_SESSION_ID',
47
+ 'OPENAI_CONVERSATION_ID',
48
+ 'SESSION_ID',
49
+ 'CI_JOB_ID'
50
+ ]);
28
51
  const permissionedFlushNetworkErrors = new Set([
29
52
  'ENOTFOUND',
30
53
  'EAI_AGAIN',
@@ -170,28 +193,20 @@ function isMaintenanceProcess(env = process.env, argv = process.argv) {
170
193
  return argv.some((arg) => /(^|[/\\])test[/\\].+\.test\.mjs$/u.test(String(arg || '')));
171
194
  }
172
195
 
173
- function stableStateKey(env = process.env) {
174
- const source =
175
- env.ALIPAY_AIPAY_SESSION_ID ||
176
- platformProjectRunId(env) ||
177
- env.CODEX_THREAD_ID ||
178
- env.CODEX_SESSION_ID ||
179
- env.CLAUDE_CODE_SESSION_ID ||
180
- env.OPENCLAW_SESSION_ID ||
181
- env.CURSOR_SESSION_ID ||
182
- env.CURSOR_TRACE_ID ||
183
- env.COZE_SESSION_ID ||
184
- env.COZE_CONVERSATION_ID ||
185
- env.HERMES_SESSION_ID ||
186
- env.OPENAI_CONVERSATION_ID ||
187
- env.SESSION_ID ||
188
- env.CI_JOB_ID ||
189
- '';
196
+ function stableStateSource(env = process.env) {
197
+ return env.ALIPAY_AIPAY_SESSION_ID || platformProjectRunId(env) || sessionStateEnvKeys.map((key) => env[key]).find(Boolean) || '';
198
+ }
199
+
200
+ function stateKeyFromSource(source) {
190
201
  if (!source) return 'default';
191
202
  if (safeText(source) && source.length <= 128) return source;
192
203
  return `key-${crypto.createHash('sha256').update(source).digest('hex')}`;
193
204
  }
194
205
 
206
+ function stableStateKey(env = process.env) {
207
+ return stateKeyFromSource(stableStateSource(env));
208
+ }
209
+
195
210
  function stateDir(env = process.env) {
196
211
  if (env.ALIPAY_AIPAY_TELEMETRY_DIR) return env.ALIPAY_AIPAY_TELEMETRY_DIR;
197
212
  return telemetryRoot();
@@ -216,7 +231,8 @@ function permissionedFlushStatePath(env = process.env) {
216
231
  if (env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE) {
217
232
  return env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE;
218
233
  }
219
- return path.join(stateDir(env), `${stableStateKey(env)}.permissioned-flush.json`);
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`);
220
236
  }
221
237
 
222
238
  export function telemetryStoragePaths(env = process.env) {
@@ -385,6 +401,13 @@ function parseNonNegativeInteger(value) {
385
401
  return Number.isSafeInteger(parsed) ? parsed : null;
386
402
  }
387
403
 
404
+ function sleepSync(ms) {
405
+ if (!Number.isFinite(ms) || ms <= 0) return;
406
+ try {
407
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(ms, 30000));
408
+ } catch {}
409
+ }
410
+
388
411
  function parseBooleanFlag(value) {
389
412
  if (typeof value === 'boolean') return value;
390
413
  if (typeof value !== 'string') return null;
@@ -394,23 +417,112 @@ function parseBooleanFlag(value) {
394
417
  return null;
395
418
  }
396
419
 
397
- function terminalExtInfoFromPayload(payload = {}, env = process.env) {
398
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
420
+ function normalizeUserInputId(value) {
421
+ if (typeof value !== 'string') return '';
422
+ const text = value.trim();
423
+ if (!text || text.length > 256 || /[\u0000-\u001f\u007f]/u.test(text)) return '';
424
+ return text;
425
+ }
426
+
427
+ function userInputHash(inputId, runId) {
428
+ const normalized = normalizeUserInputId(inputId);
429
+ if (!normalized || !safeRunId(runId)) return '';
430
+ return crypto.createHash('sha256').update(`${runId}\u0000${normalized}`).digest('hex').slice(0, 32);
431
+ }
432
+
433
+ function normalizeUserInputHashes(value) {
434
+ if (!Array.isArray(value)) return [];
435
+ return [...new Set(value.filter((entry) => typeof entry === 'string' && /^[a-f0-9]{32}$/u.test(entry)))];
436
+ }
437
+
438
+ function normalizeUserInputCounts(value) {
439
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
440
+ return { total: 0, after_flow_start: 0, flow_start_observed: false, input_hashes: [] };
441
+ }
442
+ const total = parseNonNegativeInteger(value.total ?? value.user_input_count_total);
443
+ const afterFlowStart = parseNonNegativeInteger(value.after_flow_start ?? value.user_input_count_after_flow_start);
444
+ const flowStartObserved = parseBooleanFlag(value.flow_start_observed);
445
+ if (total === null || afterFlowStart === null || flowStartObserved === null) {
446
+ return { total: 0, after_flow_start: 0, flow_start_observed: false, input_hashes: [] };
447
+ }
448
+ return {
449
+ total,
450
+ after_flow_start: Math.min(afterFlowStart, total),
451
+ flow_start_observed: flowStartObserved,
452
+ input_hashes: normalizeUserInputHashes(value.input_hashes),
453
+ flow_start_observed_at: parseNonNegativeInteger(value.flow_start_observed_at) || undefined,
454
+ updated_at: parseNonNegativeInteger(value.updated_at) || undefined
455
+ };
456
+ }
457
+
458
+ function validateTerminalCountPayload(payload = {}, env = process.env, { strict = false } = {}) {
459
+ const fail = (message) => {
460
+ if (strict) throw new Error(message);
461
+ return null;
462
+ };
399
463
  const total = parseNonNegativeInteger(payload.userInputCountTotal ?? payload.user_input_count_total);
400
464
  const afterFlowStart = parseNonNegativeInteger(
401
465
  payload.userInputCountAfterFlowStart ?? payload.user_input_count_after_flow_start
402
466
  );
403
467
  const flowStartObserved = parseBooleanFlag(payload.flowStartObserved ?? payload.flow_start_observed);
404
468
  const rootIntent = rootIntentFrom(payload, env);
405
- if (total === null || afterFlowStart === null || flowStartObserved === null) return '';
406
- if (!rootIntent) return '';
407
- if (afterFlowStart > total) return '';
408
- if (!flowStartObserved && afterFlowStart !== 0) return '';
469
+ if (total === null || afterFlowStart === null || flowStartObserved === null) {
470
+ return fail('终态用户输入计数必须是合法非负整数,flow-start-observed 必须是 true/false');
471
+ }
472
+ if (total < 1) return fail('终态 user_input_count_total 必须至少包含触发当前 run 的用户输入');
473
+ if (!rootIntent) return fail('终态用户输入计数缺少合法 root_intent');
474
+ if (afterFlowStart > total) return fail('终态 user_input_count_after_flow_start 不能大于 user_input_count_total');
475
+ if (flowStartObserved && afterFlowStart >= total) {
476
+ return fail('终态 flow start 已观测时,after-flow 用户输入数必须小于 total;请重新核对 before-flow/after-flow 分界');
477
+ }
478
+ if (!flowStartObserved && afterFlowStart !== 0) {
479
+ return fail('终态 flow start 未观测时,after-flow 用户输入数必须为 0');
480
+ }
481
+ return { total, afterFlowStart, flowStartObserved, rootIntent };
482
+ }
483
+
484
+ function initialUserInputCounts(nowMs = Date.now(), inputId = initialUserInputId, runId = '') {
485
+ return {
486
+ total: 1,
487
+ after_flow_start: 0,
488
+ flow_start_observed: false,
489
+ input_hashes: [userInputHash(inputId, runId) || userInputHash(initialUserInputId, runId)].filter(Boolean),
490
+ updated_at: nowMs
491
+ };
492
+ }
493
+
494
+ function addUserInputCount(counts, phase, inputId, runId, nowMs = Date.now()) {
495
+ const inputHash = userInputHash(inputId, runId);
496
+ if (!inputHash || counts.input_hashes.includes(inputHash)) return counts;
497
+ return {
498
+ ...counts,
499
+ total: counts.total + 1,
500
+ after_flow_start: counts.after_flow_start + (phase === 'after-flow' ? 1 : 0),
501
+ input_hashes: [...counts.input_hashes, inputHash],
502
+ updated_at: nowMs
503
+ };
504
+ }
505
+
506
+ function publicUserInputCountState(counts) {
507
+ if (!counts) return null;
508
+ const normalized = normalizeUserInputCounts(counts);
509
+ return {
510
+ total: normalized.total,
511
+ after_flow_start: normalized.after_flow_start,
512
+ flow_start_observed: normalized.flow_start_observed,
513
+ tracked_inputs: normalized.input_hashes.length
514
+ };
515
+ }
516
+
517
+ function terminalExtInfoFromPayload(payload = {}, env = process.env) {
518
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
519
+ const counts = validateTerminalCountPayload(payload, env);
520
+ if (!counts) return '';
409
521
  return JSON.stringify({
410
- user_input_count_total: total,
411
- user_input_count_after_flow_start: afterFlowStart,
412
- flow_start_observed: flowStartObserved,
413
- root_intent: rootIntent
522
+ user_input_count_total: counts.total,
523
+ user_input_count_after_flow_start: counts.afterFlowStart,
524
+ flow_start_observed: counts.flowStartObserved,
525
+ root_intent: counts.rootIntent
414
526
  });
415
527
  }
416
528
 
@@ -568,15 +680,54 @@ function parseCliArgs(argv) {
568
680
  return { command, args };
569
681
  }
570
682
 
571
- export function terminalMetricContextFromArgs(args) {
683
+ export function terminalMetricContextFromArgs(args, context = {}) {
572
684
  const optional = (name) => {
573
685
  const value = args[name];
574
686
  return typeof value === 'string' && value.length > 0 ? value : undefined;
575
687
  };
688
+ const hasCountArg = terminalCountArgNames.some((name) => Object.prototype.hasOwnProperty.call(args, name));
689
+ if (hasCountArg) {
690
+ const missing = terminalCountArgNames.filter((name) => optional(name) === undefined);
691
+ if (missing.length > 0) throw new Error(`终态用户输入计数参数必须同时提供,缺少 --${missing[0]}`);
692
+ const counts = validateTerminalCountPayload({
693
+ userInputCountTotal: optional('user-input-count-total'),
694
+ userInputCountAfterFlowStart: optional('user-input-count-after-flow-start'),
695
+ flowStartObserved: optional('flow-start-observed'),
696
+ rootIntent: args['root-intent'] || args.rootIntent || args.root_intent || context.rootIntent || context.root_intent
697
+ }, process.env, { strict: true });
698
+ return {
699
+ userInputCountTotal: counts.total,
700
+ userInputCountAfterFlowStart: counts.afterFlowStart,
701
+ flowStartObserved: counts.flowStartObserved
702
+ };
703
+ }
704
+ const state = readRunState(process.env);
705
+ const stored = normalizeUserInputCounts(state?.user_input_counts);
706
+ if (stored.total < 1) {
707
+ const fallbackCounts = validateTerminalCountPayload({
708
+ userInputCountTotal: 1,
709
+ userInputCountAfterFlowStart: 0,
710
+ flowStartObserved: Boolean(stored.flow_start_observed),
711
+ rootIntent: state?.root_intent
712
+ }, process.env);
713
+ if (!fallbackCounts) return {};
714
+ return {
715
+ userInputCountTotal: fallbackCounts.total,
716
+ userInputCountAfterFlowStart: fallbackCounts.afterFlowStart,
717
+ flowStartObserved: fallbackCounts.flowStartObserved
718
+ };
719
+ }
720
+ const counts = validateTerminalCountPayload({
721
+ userInputCountTotal: stored.total,
722
+ userInputCountAfterFlowStart: stored.after_flow_start,
723
+ flowStartObserved: stored.flow_start_observed,
724
+ rootIntent: state?.root_intent
725
+ }, process.env);
726
+ if (!counts) return {};
576
727
  return {
577
- userInputCountTotal: optional('user-input-count-total'),
578
- userInputCountAfterFlowStart: optional('user-input-count-after-flow-start'),
579
- flowStartObserved: optional('flow-start-observed')
728
+ userInputCountTotal: counts.total,
729
+ userInputCountAfterFlowStart: counts.afterFlowStart,
730
+ flowStartObserved: counts.flowStartObserved
580
731
  };
581
732
  }
582
733
 
@@ -614,11 +765,42 @@ function createRunState(args, env = process.env, nowMs = Date.now()) {
614
765
  run_id: runId,
615
766
  root_intent: rootIntent,
616
767
  product_type: productType,
768
+ user_input_counts: initialUserInputCounts(nowMs, args['input-id'] || initialUserInputId, runId),
617
769
  created_at: nowMs,
618
770
  updated_at: nowMs
619
771
  };
620
772
  }
621
773
 
774
+ function withInitialUserInputCount(state, nowMs = Date.now(), inputId = initialUserInputId) {
775
+ const counts = normalizeUserInputCounts(state?.user_input_counts);
776
+ if (counts.total >= 1) return state;
777
+ return {
778
+ ...state,
779
+ user_input_counts: initialUserInputCounts(nowMs, inputId, state.run_id),
780
+ updated_at: nowMs
781
+ };
782
+ }
783
+
784
+ function withCurrentUserInputCount(state, nowMs = Date.now(), inputId = '') {
785
+ const normalizedInputId = normalizeUserInputId(inputId);
786
+ const counts = normalizeUserInputCounts(state?.user_input_counts);
787
+ if (counts.total < 1) return withInitialUserInputCount(state, nowMs, normalizedInputId || initialUserInputId);
788
+ if (!normalizedInputId) return state;
789
+ const nextCounts = addUserInputCount(
790
+ counts,
791
+ counts.flow_start_observed ? 'after-flow' : 'before-flow',
792
+ normalizedInputId,
793
+ state.run_id,
794
+ nowMs
795
+ );
796
+ if (nextCounts === counts) return state;
797
+ return {
798
+ ...state,
799
+ user_input_counts: nextCounts,
800
+ updated_at: nowMs
801
+ };
802
+ }
803
+
622
804
  function compatibleProductType(existingProductType, requestedProductType) {
623
805
  const existing = canonicalProductType(existingProductType) || 'UNKNOWN';
624
806
  const requested = canonicalProductType(requestedProductType) || 'UNKNOWN';
@@ -655,19 +837,36 @@ function createOrReuseRunState(args, env = process.env, nowMs = Date.now()) {
655
837
  const requestedRunId = platformProjectRunId(env) || safeRunId(args['run-id']);
656
838
  const existing = readRunState(env);
657
839
  if (reusableRunState(existing, rootIntent, requestedProductType) && (!requestedRunId || existing.run_id === requestedRunId)) {
658
- return {
840
+ return withCurrentUserInputCount({
659
841
  ...existing,
660
842
  root_intent: rootIntent,
661
843
  product_type: mergeProductType(existing.product_type, requestedProductType),
662
844
  updated_at: nowMs
663
- };
845
+ }, nowMs, args['input-id']);
664
846
  }
665
847
  return createRunState(args, env, nowMs);
666
848
  }
667
849
 
850
+ function flowStartStage(stage) {
851
+ return stage === 'integration.user_confirmed' || stage === 'onboarding.user_confirmed';
852
+ }
853
+
854
+ function withFlowStartObserved(state, nowMs = Date.now()) {
855
+ const counts = normalizeUserInputCounts(state.user_input_counts);
856
+ return {
857
+ ...state,
858
+ user_input_counts: {
859
+ ...counts,
860
+ flow_start_observed: true,
861
+ flow_start_observed_at: counts.flow_start_observed_at || nowMs,
862
+ updated_at: nowMs
863
+ }
864
+ };
865
+ }
866
+
668
867
  function updateRunStateFromEvent(event, env = process.env) {
669
868
  const existing = readRunState(env) || {};
670
- const next = {
869
+ let next = {
671
870
  ...existing,
672
871
  schema_ver: 1,
673
872
  run_id: event.run_id,
@@ -676,6 +875,9 @@ function updateRunStateFromEvent(event, env = process.env) {
676
875
  created_at: existing.created_at || Date.now(),
677
876
  updated_at: Date.now()
678
877
  };
878
+ if (flowStartStage(event.stage)) {
879
+ next = withFlowStartObserved(next, Number.isFinite(Number(event.timestamp)) ? Number(event.timestamp) : Date.now());
880
+ }
679
881
  if (event.stage === 'run.terminal') {
680
882
  next.terminal = true;
681
883
  next.terminal_at = Number.isFinite(Number(event.timestamp)) ? Number(event.timestamp) : Date.now();
@@ -686,6 +888,55 @@ function updateRunStateFromEvent(event, env = process.env) {
686
888
  writeRunState(next, env);
687
889
  }
688
890
 
891
+ export function markTelemetryFlowStart(env = process.env, nowMs = Date.now()) {
892
+ if (!telemetryEnabled(env)) return null;
893
+ const existing = readRunState(env);
894
+ if (!existing?.run_id || existing.terminal) return null;
895
+ const next = withFlowStartObserved({
896
+ ...existing,
897
+ updated_at: nowMs
898
+ }, nowMs);
899
+ writeRunState(next, env);
900
+ return next.user_input_counts;
901
+ }
902
+
903
+ export function recordTelemetryUserInput(phase, env = process.env, nowMs = Date.now(), inputId = '') {
904
+ if (!telemetryEnabled(env)) return null;
905
+ if (!['before-flow', 'after-flow'].includes(phase)) {
906
+ throw new Error('--phase 仅支持 before-flow、after-flow');
907
+ }
908
+ const normalizedInputId = normalizeUserInputId(inputId);
909
+ if (!normalizedInputId) throw new Error('用户输入计数必须提供本地幂等 --input-id,且不得包含用户输入原文或敏感信息');
910
+ const existing = readRunState(env);
911
+ if (!existing?.run_id || existing.terminal) return null;
912
+ const counts = normalizeUserInputCounts(existing.user_input_counts);
913
+ if (phase === 'before-flow' && counts.flow_start_observed) {
914
+ throw new Error('当前 run 已观测 flow start,后续用户输入必须记录为 after-flow');
915
+ }
916
+ if (phase === 'after-flow' && !counts.flow_start_observed) {
917
+ throw new Error('当前 run 尚未观测 flow start,不能记录 after-flow 用户输入');
918
+ }
919
+ const inputHash = userInputHash(normalizedInputId, existing.run_id);
920
+ if (!inputHash) throw new Error('用户输入计数 input-id 无法生成本地幂等键');
921
+ if (counts.input_hashes.includes(inputHash)) return counts;
922
+ const nextCounts = addUserInputCount(counts, phase, normalizedInputId, existing.run_id, nowMs);
923
+ const next = {
924
+ ...existing,
925
+ user_input_counts: nextCounts,
926
+ updated_at: nowMs
927
+ };
928
+ writeRunState(next, env);
929
+ return nextCounts;
930
+ }
931
+
932
+ export function recordTelemetryUserInputForCurrentPhase(inputId, env = process.env, nowMs = Date.now()) {
933
+ if (!telemetryEnabled(env)) return null;
934
+ const existing = readRunState(env);
935
+ if (!existing?.run_id || existing.terminal) return null;
936
+ const counts = normalizeUserInputCounts(existing.user_input_counts);
937
+ return recordTelemetryUserInput(counts.flow_start_observed ? 'after-flow' : 'before-flow', env, nowMs, inputId);
938
+ }
939
+
689
940
  function updateExistingRunStateFromEvent(event, env = process.env) {
690
941
  const existing = readRunState(env);
691
942
  if (!existing) return false;
@@ -1098,6 +1349,17 @@ function releaseOutboxClaim(claimedPath) {
1098
1349
  }
1099
1350
  }
1100
1351
 
1352
+ function outboxTargetFilePending(file) {
1353
+ try {
1354
+ if (fs.existsSync(file)) return true;
1355
+ const dir = path.dirname(file);
1356
+ const prefix = `${path.basename(file)}.sending.`;
1357
+ return fs.readdirSync(dir).some((name) => name.startsWith(prefix));
1358
+ } catch {
1359
+ return false;
1360
+ }
1361
+ }
1362
+
1101
1363
  function readOutboxRecord(file) {
1102
1364
  try {
1103
1365
  const record = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -1153,7 +1415,7 @@ function networkUploadFailure(status) {
1153
1415
  return '';
1154
1416
  }
1155
1417
 
1156
- export function permissionedFlushDecision(env = process.env, nowMs = Date.now(), { markRequested = false } = {}) {
1418
+ export function permissionedFlushDecision(env = process.env, nowMs = Date.now()) {
1157
1419
  const outboxCount = outboxFiles(env).length;
1158
1420
  const status = readUploadStatus(env);
1159
1421
  const errorCode = networkUploadFailure(status);
@@ -1176,17 +1438,30 @@ export function permissionedFlushDecision(env = process.env, nowMs = Date.now(),
1176
1438
  };
1177
1439
  }
1178
1440
  result.needed = true;
1179
- if (markRequested) {
1180
- writePermissionedFlushState({
1181
- requested_at: nowMs,
1182
- error_code: errorCode,
1183
- outbox_count: outboxCount
1184
- }, env);
1185
- result.requested_at = nowMs;
1186
- }
1187
1441
  return result;
1188
1442
  }
1189
1443
 
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);
1459
+ if (!decision.needed || !stream || typeof stream.write !== 'function') return false;
1460
+ stream.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
1461
+ stream.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
1462
+ return true;
1463
+ }
1464
+
1190
1465
  export async function flushOutbox(env = process.env, {
1191
1466
  limit = outboxFlushLimit,
1192
1467
  targetFile = '',
@@ -1257,16 +1532,18 @@ export async function flushOutbox(env = process.env, {
1257
1532
  return result;
1258
1533
  }
1259
1534
 
1260
- function spawnOutboxFlush(env = process.env) {
1535
+ function spawnOutboxFlush(env = process.env, { delayMs = 0 } = {}) {
1261
1536
  if (!telemetryUploadEnabled(env)) return false;
1262
1537
  try {
1538
+ const childEnv = {
1539
+ ...env,
1540
+ ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || String(uploadTimeoutMs)
1541
+ };
1542
+ if (delayMs > 0) childEnv.ALIPAY_AIPAY_TELEMETRY_FLUSH_DELAY_MS = String(Math.min(delayMs, 30000));
1263
1543
  const child = spawn(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
1264
1544
  detached: true,
1265
1545
  stdio: 'ignore',
1266
- env: {
1267
- ...env,
1268
- ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || String(uploadTimeoutMs)
1269
- }
1546
+ env: childEnv
1270
1547
  });
1271
1548
  child.on('error', (error) => {
1272
1549
  writeUploadStatus({
@@ -1291,25 +1568,34 @@ function foregroundFlushTerminalOutboxFile(file, env = process.env) {
1291
1568
  if (!telemetryUploadEnabled(env) || !file) return false;
1292
1569
  const parsed = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_TERMINAL_FLUSH_TIMEOUT_MS);
1293
1570
  const timeoutMs = parsed && parsed > 0 ? Math.min(parsed, 30000) : terminalForegroundUploadTimeoutMs;
1294
- try {
1295
- execFileSync(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
1296
- stdio: 'ignore',
1297
- timeout: timeoutMs + 800,
1298
- env: {
1299
- ...env,
1300
- ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: String(timeoutMs),
1301
- ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE: file
1302
- }
1303
- });
1304
- return true;
1305
- } catch (error) {
1306
- writeUploadStatus({
1307
- status: 'failed',
1308
- error_code: safeDimension(error?.code || error?.signal || error?.name || 'TERMINAL_FLUSH_FAILED', 'TERMINAL_FLUSH_FAILED', 80),
1309
- error_message: 'terminal flush failed'
1310
- }, env);
1311
- return false;
1571
+ const parsedAttempts = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_TERMINAL_FLUSH_ATTEMPTS);
1572
+ const attempts = parsedAttempts && parsedAttempts > 0
1573
+ ? Math.min(parsedAttempts, 3)
1574
+ : terminalForegroundUploadMaxAttempts;
1575
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
1576
+ try {
1577
+ execFileSync(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
1578
+ stdio: 'ignore',
1579
+ timeout: timeoutMs + 800,
1580
+ env: {
1581
+ ...env,
1582
+ ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: String(timeoutMs),
1583
+ ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE: file
1584
+ }
1585
+ });
1586
+ } catch (error) {
1587
+ writeUploadStatus({
1588
+ status: 'failed',
1589
+ error_code: safeDimension(error?.code || error?.signal || error?.name || 'TERMINAL_FLUSH_FAILED', 'TERMINAL_FLUSH_FAILED', 80),
1590
+ error_message: 'terminal flush failed'
1591
+ }, env);
1592
+ }
1593
+ if (!outboxTargetFilePending(file)) return true;
1594
+ const retryable = Boolean(networkUploadFailure(readUploadStatus(env)));
1595
+ if (!retryable || attempt >= attempts) return false;
1596
+ sleepSync(terminalForegroundUploadRetryDelayMs);
1312
1597
  }
1598
+ return !outboxTargetFilePending(file);
1313
1599
  }
1314
1600
 
1315
1601
  function scheduleSyncLogUpload(event, env = process.env) {
@@ -1320,7 +1606,10 @@ function scheduleSyncLogUpload(event, env = process.env) {
1320
1606
  return false;
1321
1607
  }
1322
1608
  if (event?.stage === 'run.terminal') {
1323
- foregroundFlushTerminalOutboxFile(queuedFile, env);
1609
+ if (!foregroundFlushTerminalOutboxFile(queuedFile, env)) {
1610
+ spawnOutboxFlush(env, { delayMs: terminalBackgroundRetryDelayMs });
1611
+ emitPermissionedFlushNeededMarkers(env);
1612
+ }
1324
1613
  return true;
1325
1614
  }
1326
1615
  return spawnOutboxFlush(env);
@@ -1338,7 +1627,7 @@ export async function runTelemetryCli(argv = process.argv) {
1338
1627
  try {
1339
1628
  const { command, args } = parseCliArgs(argv);
1340
1629
  if (command === 'start-run') {
1341
- assertCliArgs(args, new Set(['root-intent', 'product-type', 'run-id']), 0);
1630
+ assertCliArgs(args, new Set(['root-intent', 'product-type', 'run-id', 'input-id']), 0);
1342
1631
  const state = createOrReuseRunState(args);
1343
1632
  if (!writeRunState(state) && telemetryEnabled(process.env)) {
1344
1633
  recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to write local telemetry run state');
@@ -1363,6 +1652,26 @@ export async function runTelemetryCli(argv = process.argv) {
1363
1652
  }
1364
1653
  return 0;
1365
1654
  }
1655
+ if (command === 'count-user-input' && args._.length === 0) {
1656
+ assertCliArgs(args, new Set(['phase', 'input-id']), 0);
1657
+ const counts = recordTelemetryUserInput(args.phase, process.env, Date.now(), args['input-id']);
1658
+ if (counts) {
1659
+ process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE=${JSON.stringify(publicUserInputCountState(counts))}\n`);
1660
+ } else {
1661
+ process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE_MISSING=true\n`);
1662
+ }
1663
+ return 0;
1664
+ }
1665
+ if (command === 'mark-flow-start' && args._.length === 0) {
1666
+ assertCliArgs(args, new Set(), 0);
1667
+ const counts = markTelemetryFlowStart(process.env, Date.now());
1668
+ if (counts) {
1669
+ process.stderr.write(`${internalPrefix}USER_INPUT_FLOW_START_STATE=${JSON.stringify(publicUserInputCountState(counts))}\n`);
1670
+ } else {
1671
+ process.stderr.write(`${internalPrefix}USER_INPUT_FLOW_START_STATE_MISSING=true\n`);
1672
+ }
1673
+ return 0;
1674
+ }
1366
1675
  if (command === 'upload-status' && args._.length === 0) {
1367
1676
  assertCliArgs(args, new Set(), 0);
1368
1677
  const status = readUploadStatus(process.env) || { schema_ver: 1, status: 'unknown' };
@@ -1371,7 +1680,7 @@ export async function runTelemetryCli(argv = process.argv) {
1371
1680
  }
1372
1681
  if (command === 'permissioned-flush-status' && args._.length === 0) {
1373
1682
  assertCliArgs(args, new Set(), 0);
1374
- const decision = permissionedFlushDecision(process.env, Date.now(), { markRequested: false });
1683
+ const decision = permissionedFlushDecision(process.env, Date.now());
1375
1684
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_STATUS=${JSON.stringify(decision)}\n`);
1376
1685
  if (decision.needed) {
1377
1686
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_CANDIDATE=true\n`);
@@ -1380,23 +1689,26 @@ export async function runTelemetryCli(argv = process.argv) {
1380
1689
  }
1381
1690
  if (command === 'permissioned-flush-needed' && args._.length === 0) {
1382
1691
  assertCliArgs(args, new Set(), 0);
1383
- const decision = permissionedFlushDecision(process.env, Date.now(), { markRequested: true });
1692
+ const decision = permissionedFlushDecision(process.env, Date.now());
1384
1693
  process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
1385
- if (decision.needed) {
1386
- process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
1387
- }
1694
+ if (decision.needed) process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
1388
1695
  return 0;
1389
1696
  }
1390
1697
  if (command === 'flush-outbox') {
1391
1698
  assertCliArgs(args, new Set(), 0);
1392
1699
  if (!telemetryEnabled(process.env)) return 0;
1393
- spawnOutboxFlush(process.env);
1700
+ const decision = permissionedFlushDecision(process.env, Date.now());
1701
+ if (spawnOutboxFlush(process.env)) {
1702
+ recordPermissionedFlushRequested(process.env, Date.now(), decision);
1703
+ }
1394
1704
  return 0;
1395
1705
  }
1396
1706
  if (command === 'flush-outbox-worker') {
1397
1707
  assertCliArgs(args, new Set(), 0);
1398
1708
  if (!telemetryEnabled(process.env)) return 0;
1399
1709
  const timeout = Number(process.env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || uploadTimeoutMs);
1710
+ const delay = parseNonNegativeInteger(process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_DELAY_MS);
1711
+ if (delay && delay > 0) sleepSync(Math.min(delay, 30000));
1400
1712
  await flushOutbox(process.env, {
1401
1713
  targetFile: process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE || '',
1402
1714
  timeoutMs: Number.isFinite(timeout) ? timeout : uploadTimeoutMs
@@ -1,4 +1,4 @@
1
- - 字段/测试白名单:stage/flow、产品、根意图、时间戳、Skill 版本、宿主/模型、匿名机器哈希、`run_id`、终态计数;禁密钥、账号密码、订单/交易号、截图/凭证、用户输入、路径/URL、响应、命令输出、堆栈原文。失败不影响业务;内部行 `ALIPAY_AIPAY_INTERNAL:`。
2
- - run:`start-run` 创建/复用 `run_id`,脚本继承 `PLATFORM_ID=<run_id>`;`root_intent + product_type` 判定复用/新建,`UNKNOWN` 补全产品仍复用同一 `run_id`;换产品/根意图/已终态/重新开始/重新走流程/作废本轮流程才新建。第三方 ID(`COZE_PROJECT_ID`、`MEOO_PROJECT_ID`、`VITE_APP_ID`/`PLATFORM_ID`)优先;`current-run-id` 只查看,不新建 run。outbox 落盘,collector 2xx 才删;维护/测试/Benchmark 禁上传。
3
- - 计数:`user_input_count_total` 自触发当前 run 的用户消息起;`user_input_count_after_flow_start` 自方案送达并进入正式执行后的下一条用户消息起。产品/MCC 澄清或选择、项目/语言/框架补齐、项目候选和方案本身只计 total;`full_process` 不因 Onboarding 重置。`flow_start_observed` 表示正式 flow start;用户 kill 未终态不补报。终态只追加计数,不新增确认点、stage 或独立上报。`run.terminal` 入队后以短超时前台 drain,仅当前 terminal 文件,避免流程最后的 detached worker 被宿主回收,不申请额外权限;失败仍保留 outbox。`c4=ext_info`,计数合法且有 `root_intent` 才写 JSON。
4
- - 补传:不为权限探测新增网络事件;实际低敏上传就是自然探测,宿主已允许访问 collector 时不额外申请权限。`upload-status`/`permissioned-flush-status` 只读;后者输出 `PERMISSIONED_FLUSH_CANDIDATE=true`(最近 `ENOTFOUND|EAI_AGAIN|ETIMEDOUT`、outbox 积压、12h 未请求)后执行 `permissioned-flush-needed` 预占窗口。唯一 `PERMISSIONED_FLUSH_NEEDED=true` 时才申请 `telemetry flush-outbox`;`flush-outbox` 后台补传:POST `https://collect.alipay.com/yuyan/`(`collect.alipay.com:443`,路径 `/yuyan/`)。说明:`是否允许 Agent 上报 Skill 使用过程的统计信息?只含产品类别、时间、Skill 版本、匿名设备标识、人机交互次数等用于帮助改善用户体验的非敏感信息,不会采集包含源代码、用户输入内容等任何敏感信息。` 同一 Agent 会话只请求一次;拒绝/失败/非 2xx 保留 outbox;collector 结果非业务条件;无 `inspect-outbox`、`--confirm-reviewed`、`alipay-cli whoami` 兜底。
1
+ - 字段白名单:stage/flow、产品、根意图、时间戳、Skill 版本、宿主/模型、匿名机器哈希、`run_id`、终态计数;禁密钥/账号密码/订单/交易号/截图/凭证/用户输入/路径/URL/响应/命令输出/堆栈原文;失败不影响业务,内部行 `ALIPAY_AIPAY_INTERNAL:`。
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` 兜底。
@@ -56,10 +56,10 @@ Step 1: 方案规划 → Step 2: 环境检查 → Step 3: 登录授权
56
56
  <!-- GENERATED:ONBOARDING-GUARDS-SHARED:START -->
57
57
  共享护栏:`P-MCP-CONTRACT-FROZEN` 仅用已验证脚本;禁推断或尝试近似方法;`P-AUTH-BEFORE-ONBOARDING` 登录/scope/MCC 未通过前禁查询和写入;`P-SENSITIVE-STATE` 公钥限当前 app 调用且禁复述/保存;敏感值禁入回复/摘要/日志/状态/宽权限文件;临时 URL 仅由登记动作展示;`P-CUSTOMER-OUTPUT-EXCLUSIVE` 登记 stdout 原文是唯一正文;阻塞送达后立即等待;`P-ONBOARDING-CONTROL-CHANNEL` 内部行禁对客;正文为空按唯一终态转移;`P-DETERMINISTIC-HANDOFF` 仅执行登记命令并按唯一终态转移;禁追加解析;失败只走登记恢复。
58
58
  <!-- GENERATED:ONBOARDING-GUARDS-SHARED:END -->
59
- 环境检查、产品/MCC 澄清或方案展示前先执行平台入口。`onboarding_only` 正常由 `onboarding-bootstrap --root-intent onboarding_only` 包含本动作和 entry 加载;仅原子 fallback 或 `full_process` 衔接时执行:
59
+ 环境检查、产品/MCC 澄清或方案展示前先执行平台入口。`onboarding_only` 正常由 `onboarding-bootstrap --root-intent onboarding_only --input-id "$USER_INPUT_ID"` 包含本动作和 entry 加载;仅原子 fallback 或 `full_process` 衔接时执行:
60
60
 
61
61
  ```bash
62
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry --user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>
62
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry
63
63
  ```
64
64
 
65
65
  - Windows:runner 从私有 run context 取 `run_id` 生成 `platform_id`,禁止 Agent 传入或拼接;消息渲染即终结 run,送达唯一 stdout 后停止 Onboarding;`full_process` 保留 Integration 结果。
@@ -68,7 +68,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message plat
68
68
  用户明确要用企业账号通过 Skill 开通时执行下列消息并停止 Onboarding;不得主动询问账号类型,也不得从 MCC、营业执照或“公司”等描述推断。
69
69
 
70
70
  ```bash
71
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message enterprise-unsupported --variant <ONBOARDING_ONLY|FULL_PROCESS> --user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>
71
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message enterprise-unsupported --variant <ONBOARDING_ONLY|FULL_PROCESS>
72
72
  ```
73
73
 
74
74
  `onboarding_only` 固定 `ONBOARDING_ONLY`,`full_process` 固定 `FULL_PROCESS`。原文送达最终 stdout 后停止,不执行 Step 6 或追加收口。
@@ -121,7 +121,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message mcc-
121
121
 
122
122
  `salesCode`、`scope` 和材料要求只取“产品映射”。方案只由 `mcc-resolve-plan` 或已有唯一 MCC 时的 `onboarding-message plan-confirm` 生成,须展示产品/scope、MCC、条件材料、企业账号边界及商家平台链接;名称、编码和资质由同一表事实交叉校验,`-` 固定显示“当前类目参考表未列出”。禁止补充模板外资格或审核规则。
123
123
 
124
- 不得保留“先运行旧字符串搜索,无匹配后再临时让 Agent 猜”的两阶段流程,或重复询问已知信息。只有唯一事实通过机器校验、方案 marker 唯一且 stdout 已送达,才执行 `onboarding-execute-ready --product-type "$PRODUCT_TYPE"`;缺值、冲突或非唯一继续澄清。产品/MCC 变化时重新校验并渲染。execute loader 记录兼容 stage `onboarding.user_confirmed`,不代表新增用户回复,禁止手工上报。after-flow 从此后下一条用户消息开始;前置澄清、候选和方案只计 total。`full_process` 不在本阶段重置。
124
+ 不得保留“先运行旧字符串搜索,无匹配后再临时让 Agent 猜”的两阶段流程,或重复询问已知信息。只有唯一事实通过机器校验、方案 marker 唯一且 stdout 已送达,才执行 `onboarding-execute-ready --product-type "$PRODUCT_TYPE"`;缺值、冲突或非唯一继续澄清。产品/MCC 变化时重新校验并渲染。execute loader 记录兼容 stage `onboarding.user_confirmed` 并自动标记计数 flow start,不代表新增用户回复,禁止手工上报。after-flow 从此后下一条用户消息开始;前置澄清、候选和方案只计 total。`full_process` 不在本阶段重置。
125
125
 
126
126
  <!-- ALIPAY_AIPAY_FLOW:ONBOARDING_ENTRY:END -->
127
127
 
@@ -191,7 +191,7 @@ Step 1 的产品/MCC 唯一事实校验和方案送达、Step 2 的环境 READY
191
191
  ├─ AUTH_FLOW:RETRY_WITH_LOCAL_FS_PERMISSION → 保留当前授权页短时状态,取得 CLI 私有状态目录权限后原样重试当前 wait 命令一次
192
192
  ├─ AUTH_FLOW:FAILED → 停止并按错误输出恢复,禁止进入 Step 3.1
193
193
  └─ AUTH_FLOW:SCOPE_MISMATCH / MCC_MISMATCH → 执行第 4 项
194
- 3. 仅 PENDING 后把用户原始回复传给 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-recovery auth-confirm --reply "$USER_REPLY" --scope "$SCOPE" --sales-code "$SALES_CODE" --mcc-code "$MCC_CODE" --product-name "$PRODUCT_NAME" --mcc-name "$MCC_NAME"`;runner 复用统一肯定语义判定(包括 `1`、“继续”“好了”“已完成”“OK”等),接受后只执行一次原 `auth.sh confirm`。否定、疑问、取消、修改意见或含糊输入返回内部拒绝且不得查询;成功后的转移同第 2 项,PENDING 继续等待且不重启轮询
194
+ 3. 仅 PENDING 后把用户原始回复传给 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-recovery auth-confirm --reply "$USER_REPLY" --input-id "$USER_INPUT_ID" --scope "$SCOPE" --sales-code "$SALES_CODE" --mcc-code "$MCC_CODE" --product-name "$PRODUCT_NAME" --mcc-name "$MCC_NAME"`;runner 复用统一肯定语义判定(包括 `1`、“继续”“好了”“已完成”“OK”等)并按当前用户消息幂等计数,接受后只执行一次原 `auth.sh confirm`。否定、疑问、取消、修改意见或含糊输入返回内部拒绝且不得查询;成功后的转移同第 2 项,PENDING 继续等待且不重启轮询
195
195
  4. node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" auth mismatch --defer-open --scope "$SCOPE" --sales-code "$SALES_CODE" --mcc-code "$MCC_CODE" --product-name "$PRODUCT_NAME" --mcc-name "$MCC_NAME"
196
196
  ├─ AUTH_FLOW:READY_TO_OPEN → 输出非阻塞授权页并按第 2 项执行 open + wait
197
197
  ├─ AUTH_FLOW:CONTEXT_MISMATCH → 未执行 logout;使用当前已确认的完整参数重新执行第 1 项
@@ -446,8 +446,8 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" app open-key-page "<app
446
446
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" app verify-key-and-audit --product-type "$PRODUCT_TYPE" "<本轮create输出的appId>" "<同一publicKey>"
447
447
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" app verify-key --product-type "$PRODUCT_TYPE" "<appId>" "<同一publicKey>"
448
448
  # 仅轮询耗尽且用户明确已完成;单查一次
449
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-recovery application-key --reply "$USER_REPLY" --product-type "$PRODUCT_TYPE" --mode verify-key-and-audit --app-id "<本轮create输出的appId>" --public-key "<同一publicKey>"
450
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-recovery application-key --reply "$USER_REPLY" --product-type "$PRODUCT_TYPE" --mode verify-key --app-id "<appId>" --public-key "<同一publicKey>"
449
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-recovery application-key --reply "$USER_REPLY" --input-id "$USER_INPUT_ID" --product-type "$PRODUCT_TYPE" --mode verify-key-and-audit --app-id "<本轮create输出的appId>" --public-key "<同一publicKey>"
450
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-recovery application-key --reply "$USER_REPLY" --input-id "$USER_INPUT_ID" --product-type "$PRODUCT_TYPE" --mode verify-key --app-id "<appId>" --public-key "<同一publicKey>"
451
451
  node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" app audit --product-type "$PRODUCT_TYPE" "<appId>"
452
452
  ```
453
453
 
@@ -501,9 +501,11 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message appl
501
501
  `SIGNING_RESULT`、`SERVICE_RESULT`、`APPLICATION_RESULT` 和 `INTEGRATION_RESULT` 必须使用消息目录允许的状态枚举原文,禁止拼接 `appId`、公钥文件路径、错误码、截图证据或其他自由文本;支付宝公钥保存路径只允许在已取得唯一 `ALIPAY_PUBLIC_KEY_EXPORT_STATUS=EXPORTED` 和 `ALIPAY_PUBLIC_KEY_FILE` 时,通过 `APP_KEY_SAVE_PATH` 或 `APPLICATION_OPERATION_APP_KEY_SAVE_PATH` 独立传入结果 runner。这些证据不能污染分支结果枚举。
502
502
 
503
503
  ```bash
504
- node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message closeout --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" --signing-result "$SIGNING_RESULT" --application-result "$APPLICATION_RESULT" --remaining-actions "$REMAINING_ACTIONS" [--integration-result "$INTEGRATION_RESULT"] [--service-result "$SERVICE_RESULT"] [--sandbox-config-state "$SANDBOX_CONFIG_STATE"] [--manual-verification-items "$MANUAL_VERIFICATION_ITEMS"] [--application-operation-app-id "$APPLICATION_OPERATION_APP_ID" [--application-operation-next-action "$APPLICATION_OPERATION_NEXT_ACTION"] [--application-operation-app-key-save-path "$APPLICATION_OPERATION_APP_KEY_SAVE_PATH"]] --user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>
504
+ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message closeout --product-type "$PRODUCT_TYPE" --root-intent "$ROOT_INTENT" --signing-result "$SIGNING_RESULT" --application-result "$APPLICATION_RESULT" --remaining-actions "$REMAINING_ACTIONS" [--integration-result "$INTEGRATION_RESULT"] [--service-result "$SERVICE_RESULT"] [--sandbox-config-state "$SANDBOX_CONFIG_STATE"] [--manual-verification-items "$MANUAL_VERIFICATION_ITEMS"] [--application-operation-app-id "$APPLICATION_OPERATION_APP_ID" [--application-operation-next-action "$APPLICATION_OPERATION_NEXT_ACTION"] [--application-operation-app-key-save-path "$APPLICATION_OPERATION_APP_KEY_SAVE_PATH"]]
505
505
  ```
506
506
 
507
+ 用户输入计数默认由 telemetry run state 派生,不在 closeout 事后回溯。只有本地计数状态不可用且已重新核对时,才在 closeout 命令末尾同时追加 `--user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>`;runner 会拒绝明显不合理计数。
508
+
507
509
  runner 从 `PRODUCT_TYPE` 派生产品名,网站支付/APP 支付派生服务结果“无需处理”,从 `ROOT_INTENT` 派生代码开发栏和后续衔接;标题只按完成条件派生。`onboarding_only` 省略 `--integration-result`、`--sandbox-config-state` 和 `--manual-verification-items`。`full_process`:代码开发通过填“已完成”并传 `READY`;仅沙箱待配置且其余代码与安全检查通过填“部分完成,沙箱配置待完成”并传 `CREATE_PENDING|VERIFY_PENDING`;沙箱 `READY` 且只剩非阻塞人工待验证项时填“部分完成,人工待验证”,传 `READY`,把 checklist `manualItems` 原样作为 `MANUAL_VERIFICATION_ITEMS`,并在 `REMAINING_ACTIONS` 写明实际待办,禁止传“无”。runner 拒绝旧完整参数、stdin JSON、`--input-json`、可派生字段和空字符串伪值。renderer 校验结果与沙箱状态/人工待验证项一致,并派生恢复口令或人工待验证展示;不增加确认点。不得在 stdout 前后追加衔接提醒,不伪造本轮代码修改、沙箱创建或测试结果,也不重复进入 integration。
508
510
 
509
511
  若 `full_process` 最后一跳紧接应用创建、审核提交或复用,不再先输出 `application.operation.result` 再立刻 closeout;只记录同一 `APPLICATION_OPERATION_APP_ID`,runner 复用 `APPLICATION_RESULT`,非“需人工配置”还记录 `APPLICATION_OPERATION_NEXT_ACTION`;“需人工配置”的下一步由 runner 派生。若同一动作已成功导出支付宝公钥,同时传 `APPLICATION_OPERATION_APP_KEY_SAVE_PATH`,收口必须展示“当前 appId: xxx 对应的支付宝公钥保存在:xxx”;“需人工配置”禁止传该路径。已上线且无保存路径时不展开“应用分支结果”;已上线且有保存路径时只展开 appId、状态和路径;其他应用结果合并展示并保留完成态红线。`onboarding_only`、应用分支仍需用户补材料/确认,或应用操作结果不是紧邻 Step 6 收口时,仍按应用模块输出 `application.operation.result`。合并展示不得改写完成条件,待审核、待设置公钥或待人工配置仍不得表述为应用发布完成。
@@ -32,7 +32,7 @@ CLI `login --complete` 成功只表示页面侧确认完成,不代表本机已
32
32
 
33
33
  授权页只允许 `https://aipay.alipay.com/cli-auth`,必含唯一 `deviceCode/productCode/mccCode`,只允许可选 `platform`;禁止 CLI `verification_url`、额外参数、重复参数和 fragment。页面 stdout 必须先作为独立对客消息送达,再执行 `auth.sh open` 和 `auth.sh wait`,后续 pending 或收口不得合并进同一回复。
34
34
 
35
- `wait` 生产最多查询 12 次,未成功的相邻查询间隔 5 秒,成功或过期立即停止。耗尽后把用户原始回复交给统一 runtime 的 `onboarding-recovery auth-confirm`;runner 复用统一肯定语义判定器,接受 `继续`、`好了`、`已完成`、`确认`、`OK` 和兼容输入 `1`,每次接受只执行一次 `auth.sh confirm`,不重启自动轮询。否定、疑问、取消、修改意见或含糊输入不查询。
35
+ `wait` 生产最多查询 12 次,未成功的相邻查询间隔 5 秒,成功或过期立即停止。耗尽后按 flow 登记命令把用户原始回复和当前 `USER_INPUT_ID` 交给统一 runtime 的 `onboarding-recovery auth-confirm`;runner 复用统一肯定语义判定器并对该用户消息幂等计数,接受 `继续`、`好了`、`已完成`、`确认`、`OK` 和兼容输入 `1`,每次接受只执行一次 `auth.sh confirm`,不重启自动轮询。否定、疑问、取消、修改意见或含糊输入不查询。
36
36
 
37
37
  授权对客正文只由 `auth.sh` 调用消息目录生成。初始页面不得提前要求回复完成;pending 的自然语言提示、允许回复和授权页返回提示都不在本模块维护副本。
38
38
 
@@ -30,7 +30,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
30
30
 
31
31
  动态 JSON 只取本轮事实;无变量传 `{}`,禁止字符串替换或 `eval`。正常路径禁止预跑 `render --schema` 或用 `jq`/消息目录探测字段。renderer 返回 `MESSAGE_RENDER_ERROR` 时必须先复查同一 `messageId` + `variant` schema,禁止猜枚举;非 0 即停止,不手写兜底。
32
32
 
33
- 阻塞点展示登记摘要后,把用户原始回复交给 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" reply confirm --reply "$USER_REPLY"`;仅唯一 `ALIPAY_AIPAY_INTERNAL:REPLY_DECISION=CONFIRMED` 可推进。`1`、`继续/继续推进`、`好了/已完成`、`确认/确定/同意`、`OK` 等明确肯定表达等价;否定、疑问、取消、修改或含糊回复不确认。确认只绑定当前产品、项目、主体、候选和摘要,任一变化即失效;此校验不新增操作点。
33
+ 阻塞点展示登记摘要后,把用户原始回复交给 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" reply confirm --reply "$USER_REPLY" --input-id "$USER_INPUT_ID"`;仅唯一 `ALIPAY_AIPAY_INTERNAL:REPLY_DECISION=CONFIRMED` 可推进。`1`、`继续/继续推进`、`好了/已完成`、`确认/确定/同意`、`OK` 等明确肯定表达等价;否定、疑问、取消、修改或含糊回复不确认。确认只绑定当前产品、项目、主体、候选和摘要,任一变化即失效;此校验不新增操作点。
34
34
 
35
35
  `<SKILL_DIR>` 是启动器确定的当前 Skill 规范化绝对路径占位符,不是 shell 变量。Skill 自有受控 Node、Shell 和 Python 入口必须使用文档登记的 `<SKILL_DIR>/references/...` 绝对路径单命令执行,禁止前置 `cd`、拼 `&&`、管道或退回相对路径;这些入口不新增权限确认或业务操作点。未登记命令和用户项目命令仍使用所在 Markdown 目录或已确认项目根,不得借用 Skill 权限。
36
36
 
@@ -40,15 +40,19 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
40
40
 
41
41
  | 意图 | 判定 | 入口 | 后续按需加载 |
42
42
  |---|---|---|---|
43
- | `integration_only` | 只要求代码开发、支付集成或集成排障,未同时要求产品开通 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-bootstrap --root-intent integration_only` | 当前阶段、产品和语言的契约、示例与脚本 |
44
- | `onboarding_only` | 只要求签约、入驻或产品开通,未同时要求代码开发 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-bootstrap --root-intent onboarding_only --user-input-count-total <total> --user-input-count-after-flow-start <afterFlowStart> --flow-start-observed <true|false>` | 当前阶段的授权、查询或写分支模块 |
43
+ | `integration_only` | 只要求代码开发、支付集成或集成排障,未同时要求产品开通 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" integration-bootstrap --root-intent integration_only --input-id "$USER_INPUT_ID"` | 当前阶段、产品和语言的契约、示例与脚本 |
44
+ | `onboarding_only` | 只要求签约、入驻或产品开通,未同时要求代码开发 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-bootstrap --root-intent onboarding_only --input-id "$USER_INPUT_ID"` | 当前阶段的授权、查询或写分支模块 |
45
45
  | `full_process` | 明确要求完整接入、一站式,或同一请求同时要求代码开发和产品开通 | `normal/full-process-routing.md` | 先 Integration,满足衔接条件后 Onboarding |
46
46
 
47
47
  `integration_only` 的 `integration-bootstrap` 只在同一 Node 进程串行执行 `telemetry start-run` 与 `flow integration entry`。`onboarding_only` 的 `onboarding-bootstrap` 只串行执行 `telemetry start-run`、`onboarding-message platform-entry` 与 `flow onboarding entry`。两者均为既有无业务写入动作,执行后不得重复原子命令。Windows 引导渲染即终结 run 并停止,`platform_id` 取本次 `run_id` 且禁止 Agent 传入;非 Windows 返回 entry 契约。`full_process` 不使用 `integration-bootstrap`;衔接 Onboarding 也不使用带 `start-run` 的 bootstrap,仍执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry` 后按需加载 entry,避免重复 `run.intent_detected`。
48
48
 
49
- `full_process` 识别根意图后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry start-run --root-intent full_process`;产品已唯一确定时追加 `--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 异步记录。
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` 可关闭 `METRIC_EVENT`、outbox、上传和 flush;私有 run context 仍供 `PLATFORM_ID` 与终态生命周期使用。落盘/网络/上传/flush 失败静默,不改变业务、消息或确认,stdout 为空且 stderr 内部行不对客。`integration_only` 由 checklist 终结;Onboarding 由 closeout 或登记的提前终止消息终结。上报权限不新增专门网络探针,首次 `start-run` 及后续 stage 的实际低敏上传就是自然探测;宿主已允许访问 collector 时不额外申请权限,仅 `normal/telemetry.md` 的只读状态命令确认网络失败和 outbox 积压后,才允许一次补传权限申请。Agent 按 `normal/telemetry.md` 维护本轮用户输入计数,只在终态命令追加参数,不新增业务确认点或独立上报;入队后当前 `run.terminal` 文件做短超时前台 drain,失败仍保留 outbox 且不影响业务收口。after-flow 以方案后 `*.user_confirmed` 为起点;前置澄清和候选选择不计入。脚本始终以当前 `run_id` 为 `PLATFORM_ID`,Agent 不手写环境前缀或改 MCP payload;维护诊断、outbox、上传和 flush 细节只在 `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 可达时不申请。任一终态后若 `run.terminal`/outbox 因 DNS、连接或超时失败积压,按 `normal/telemetry.md` 先本地状态判断,唯一需要时才申请一次补传权限。
52
+
53
+ 用户输入计数是入口前置职责,统计窗口从首次触发本 Skill 的用户消息开始,到当前 `run_id` 的 `run.terminal` 事件生成为止。只计用户在 Agent 对话框发送的消息,不计宿主/系统/网页授权、扫码、Skill stage、runner、工具调用或自动轮询。文字、图片/文件上传、空白或短确认消息、材料补充、确认回复和中途插话都按一条用户消息计数;同一条消息同时包含文字和图片/文件时仍只计一条。`USER_INPUT_ID` 是当前用户消息的本地幂等键:Agent 从首次触发本 Skill 的用户消息开始,在当前对话内给每条新用户消息分配单调序号,并在同一消息的重试中复用同一值。`USER_INPUT_ID` 不得包含或派生自用户输入内容、文件名、路径、截图/OCR、业务材料、stage 名或工具调用序号;运行时只保存其本地 hash,不上报。
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` 按需读取。
52
56
 
53
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 输入。
54
58