@alipay/alipay-aipay 1.6.2-beta3 → 1.6.3-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skill/SKILL.md +13 -9
- package/skill/VERSION +1 -1
- package/skill/references/normal/scripts/integration_message_runner.mjs +11 -2
- package/skill/references/normal/scripts/interaction_reply.mjs +9 -1
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +16 -3
- package/skill/references/normal/scripts/onboarding_recovery_runner.mjs +3 -1
- package/skill/references/normal/scripts/runtime.mjs +6 -4
- package/skill/references/normal/scripts/telemetry.mjs +237 -79
- package/skill/references/normal/telemetry.md +3 -3
- package/skill/references/onboarding/flow.md +4 -4
- package/skill/references/onboarding/modules/authorization.md +1 -1
- package/skill/references/runtime-entry.md +9 -5
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -5,23 +5,27 @@ description: >-
|
|
|
5
5
|
触发关键词:"AI支付"、"接支付宝"、"一站式接支付宝"、"接个支付"、"开发支付"、"402收款"、"AI按量付费"、"AI网页应用收款"、"AI移动应用收款"、"按量付费"、"快捷收款"、"快捷收单"、"网站支付"、"APP 支付"、"产品开通"、"商家开通"、"签约入驻"等。
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# 支付宝 AI 付 Skill 启动器
|
|
9
9
|
|
|
10
|
-
|
|
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`
|
|
20
|
+
`SKILL_DIR` 必须绑定刚触发且正在执行本命令的 Skill 规范化绝对路径,不得换成 Agent 启动目录、用户项目目录、用户 Home、npm 包源码根或另一份 Skill 副本。`VERSION` 缺失/非法或 `references/runtime-entry.md` 缺失时由本命令尝试精确修复;命令失败或 marker 无法唯一解析时按 `SELF_UPDATE:CHECK_FAILED`。
|
|
17
21
|
|
|
18
|
-
stderr
|
|
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`
|
|
29
|
+
除 `RETRY_WITH_NETWORK` 的一次原样重试外,只完整读取同一 `SKILL_DIR` 的 `references/runtime-entry.md`,在当前上下文记录 `ALIPAY_AIPAY_RUNTIME_ENTRY_LOADED=true` 和规范化路径,再按原请求路由。`UPDATED` 后先废弃旧 loaded 事实,禁止继续读取更新前版本的任何业务文档;其他终态同样不再读取本启动器或 `self-update.md` 补充规则。
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
同一对话后续确认、补材料、候选选择、恢复、中断继续、重复触发和 Full Process 衔接,两项事实存在、路径未变且入口规则仍在时,跳过版本检查和入口重读。仅 `UPDATED`、路径变化或上下文压缩导致规则不可用时重读并刷新;事实不落盘。
|
package/skill/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.6.
|
|
1
|
+
1.6.3-beta
|
|
@@ -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, {
|
|
1369
|
+
...terminalMetricContextFromArgs(args, {
|
|
1370
|
+
rootIntent: 'integration_only',
|
|
1371
|
+
terminalReason: terminalReasonForChecklist(input)
|
|
1372
|
+
})
|
|
1364
1373
|
});
|
|
1365
1374
|
}
|
|
1366
1375
|
return rendered;
|
|
@@ -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;
|
|
@@ -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(
|
|
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, {
|
|
767
|
+
...terminalMetricContextFromArgs(args, {
|
|
768
|
+
rootIntent: args['root-intent'],
|
|
769
|
+
terminalReason
|
|
770
|
+
})
|
|
758
771
|
});
|
|
759
772
|
return rendered;
|
|
760
773
|
}
|
|
@@ -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);
|
|
@@ -22,16 +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
|
+
]);
|
|
40
|
+
const initialUserInputId = 'run.start';
|
|
30
41
|
const outboxFlushLimit = 50;
|
|
31
42
|
const outboxClaimTtlMs = 5 * 60 * 1000;
|
|
32
43
|
const outboxMaxRecords = 1000;
|
|
33
44
|
const outboxMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
|
|
34
|
-
const permissionedFlushTtlMs = 12 * 60 * 60 * 1000;
|
|
35
45
|
const platformProjectIdEnvKeys = Object.freeze(['COZE_PROJECT_ID', 'MEOO_PROJECT_ID']);
|
|
36
46
|
const sessionStateEnvKeys = Object.freeze([
|
|
37
47
|
'CODEX_THREAD_ID',
|
|
@@ -226,21 +236,12 @@ function uploadStatusPath(env = process.env) {
|
|
|
226
236
|
return path.join(stateDir(env), 'upload-status.json');
|
|
227
237
|
}
|
|
228
238
|
|
|
229
|
-
function permissionedFlushStatePath(env = process.env) {
|
|
230
|
-
if (env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE) {
|
|
231
|
-
return env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE;
|
|
232
|
-
}
|
|
233
|
-
const source = stableStateSource(env) || (Object.keys(env).length > 0 ? readRunState(env)?.run_id : '') || '';
|
|
234
|
-
return path.join(stateDir(env), `${stateKeyFromSource(source)}.permissioned-flush.json`);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
239
|
export function telemetryStoragePaths(env = process.env) {
|
|
238
240
|
return {
|
|
239
241
|
state_dir: stateDir(env),
|
|
240
242
|
run_state_file: runStatePath(env),
|
|
241
243
|
outbox_dir: outboxDir(env),
|
|
242
|
-
upload_status_file: uploadStatusPath(env)
|
|
243
|
-
permissioned_flush_state_file: permissionedFlushStatePath(env)
|
|
244
|
+
upload_status_file: uploadStatusPath(env)
|
|
244
245
|
};
|
|
245
246
|
}
|
|
246
247
|
|
|
@@ -416,20 +417,45 @@ function parseBooleanFlag(value) {
|
|
|
416
417
|
return null;
|
|
417
418
|
}
|
|
418
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
|
+
|
|
426
|
+
function normalizeUserInputId(value) {
|
|
427
|
+
if (typeof value !== 'string') return '';
|
|
428
|
+
const text = value.trim();
|
|
429
|
+
if (!text || text.length > 256 || /[\u0000-\u001f\u007f]/u.test(text)) return '';
|
|
430
|
+
return text;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function userInputHash(inputId, runId) {
|
|
434
|
+
const normalized = normalizeUserInputId(inputId);
|
|
435
|
+
if (!normalized || !safeRunId(runId)) return '';
|
|
436
|
+
return crypto.createHash('sha256').update(`${runId}\u0000${normalized}`).digest('hex').slice(0, 32);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function normalizeUserInputHashes(value) {
|
|
440
|
+
if (!Array.isArray(value)) return [];
|
|
441
|
+
return [...new Set(value.filter((entry) => typeof entry === 'string' && /^[a-f0-9]{32}$/u.test(entry)))];
|
|
442
|
+
}
|
|
443
|
+
|
|
419
444
|
function normalizeUserInputCounts(value) {
|
|
420
445
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
421
|
-
return { total: 0, after_flow_start: 0, flow_start_observed: false };
|
|
446
|
+
return { total: 0, after_flow_start: 0, flow_start_observed: false, input_hashes: [] };
|
|
422
447
|
}
|
|
423
448
|
const total = parseNonNegativeInteger(value.total ?? value.user_input_count_total);
|
|
424
449
|
const afterFlowStart = parseNonNegativeInteger(value.after_flow_start ?? value.user_input_count_after_flow_start);
|
|
425
450
|
const flowStartObserved = parseBooleanFlag(value.flow_start_observed);
|
|
426
451
|
if (total === null || afterFlowStart === null || flowStartObserved === null) {
|
|
427
|
-
return { total: 0, after_flow_start: 0, flow_start_observed: false };
|
|
452
|
+
return { total: 0, after_flow_start: 0, flow_start_observed: false, input_hashes: [] };
|
|
428
453
|
}
|
|
429
454
|
return {
|
|
430
455
|
total,
|
|
431
456
|
after_flow_start: Math.min(afterFlowStart, total),
|
|
432
457
|
flow_start_observed: flowStartObserved,
|
|
458
|
+
input_hashes: normalizeUserInputHashes(value.input_hashes),
|
|
433
459
|
flow_start_observed_at: parseNonNegativeInteger(value.flow_start_observed_at) || undefined,
|
|
434
460
|
updated_at: parseNonNegativeInteger(value.updated_at) || undefined
|
|
435
461
|
};
|
|
@@ -461,15 +487,51 @@ function validateTerminalCountPayload(payload = {}, env = process.env, { strict
|
|
|
461
487
|
return { total, afterFlowStart, flowStartObserved, rootIntent };
|
|
462
488
|
}
|
|
463
489
|
|
|
490
|
+
function initialUserInputCounts(nowMs = Date.now(), inputId = initialUserInputId, runId = '') {
|
|
491
|
+
return {
|
|
492
|
+
total: 1,
|
|
493
|
+
after_flow_start: 0,
|
|
494
|
+
flow_start_observed: false,
|
|
495
|
+
input_hashes: [userInputHash(inputId, runId) || userInputHash(initialUserInputId, runId)].filter(Boolean),
|
|
496
|
+
updated_at: nowMs
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function addUserInputCount(counts, phase, inputId, runId, nowMs = Date.now()) {
|
|
501
|
+
const inputHash = userInputHash(inputId, runId);
|
|
502
|
+
if (!inputHash || counts.input_hashes.includes(inputHash)) return counts;
|
|
503
|
+
return {
|
|
504
|
+
...counts,
|
|
505
|
+
total: counts.total + 1,
|
|
506
|
+
after_flow_start: counts.after_flow_start + (phase === 'after-flow' ? 1 : 0),
|
|
507
|
+
input_hashes: [...counts.input_hashes, inputHash],
|
|
508
|
+
updated_at: nowMs
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function publicUserInputCountState(counts) {
|
|
513
|
+
if (!counts) return null;
|
|
514
|
+
const normalized = normalizeUserInputCounts(counts);
|
|
515
|
+
return {
|
|
516
|
+
total: normalized.total,
|
|
517
|
+
after_flow_start: normalized.after_flow_start,
|
|
518
|
+
flow_start_observed: normalized.flow_start_observed,
|
|
519
|
+
tracked_inputs: normalized.input_hashes.length
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
464
523
|
function terminalExtInfoFromPayload(payload = {}, env = process.env) {
|
|
465
524
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
|
|
466
525
|
const counts = validateTerminalCountPayload(payload, env);
|
|
467
526
|
if (!counts) return '';
|
|
527
|
+
const terminalReason = normalizeTerminalReason(payload.terminalReason ?? payload.terminal_reason);
|
|
528
|
+
if (!terminalReason) return '';
|
|
468
529
|
return JSON.stringify({
|
|
469
530
|
user_input_count_total: counts.total,
|
|
470
531
|
user_input_count_after_flow_start: counts.afterFlowStart,
|
|
471
532
|
flow_start_observed: counts.flowStartObserved,
|
|
472
|
-
root_intent: counts.rootIntent
|
|
533
|
+
root_intent: counts.rootIntent,
|
|
534
|
+
terminal_reason: terminalReason
|
|
473
535
|
});
|
|
474
536
|
}
|
|
475
537
|
|
|
@@ -632,6 +694,11 @@ export function terminalMetricContextFromArgs(args, context = {}) {
|
|
|
632
694
|
const value = args[name];
|
|
633
695
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
634
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 };
|
|
635
702
|
const hasCountArg = terminalCountArgNames.some((name) => Object.prototype.hasOwnProperty.call(args, name));
|
|
636
703
|
if (hasCountArg) {
|
|
637
704
|
const missing = terminalCountArgNames.filter((name) => optional(name) === undefined);
|
|
@@ -643,14 +710,31 @@ export function terminalMetricContextFromArgs(args, context = {}) {
|
|
|
643
710
|
rootIntent: args['root-intent'] || args.rootIntent || args.root_intent || context.rootIntent || context.root_intent
|
|
644
711
|
}, process.env, { strict: true });
|
|
645
712
|
return {
|
|
713
|
+
rootIntent: counts.rootIntent,
|
|
646
714
|
userInputCountTotal: counts.total,
|
|
647
715
|
userInputCountAfterFlowStart: counts.afterFlowStart,
|
|
648
|
-
flowStartObserved: counts.flowStartObserved
|
|
716
|
+
flowStartObserved: counts.flowStartObserved,
|
|
717
|
+
...reasonContext
|
|
649
718
|
};
|
|
650
719
|
}
|
|
651
720
|
const state = readRunState(process.env);
|
|
652
721
|
const stored = normalizeUserInputCounts(state?.user_input_counts);
|
|
653
|
-
if (stored.total < 1)
|
|
722
|
+
if (stored.total < 1) {
|
|
723
|
+
const fallbackCounts = validateTerminalCountPayload({
|
|
724
|
+
userInputCountTotal: 1,
|
|
725
|
+
userInputCountAfterFlowStart: 0,
|
|
726
|
+
flowStartObserved: Boolean(stored.flow_start_observed),
|
|
727
|
+
rootIntent: state?.root_intent
|
|
728
|
+
}, process.env);
|
|
729
|
+
if (!fallbackCounts) return {};
|
|
730
|
+
return {
|
|
731
|
+
rootIntent: fallbackCounts.rootIntent,
|
|
732
|
+
userInputCountTotal: fallbackCounts.total,
|
|
733
|
+
userInputCountAfterFlowStart: fallbackCounts.afterFlowStart,
|
|
734
|
+
flowStartObserved: fallbackCounts.flowStartObserved,
|
|
735
|
+
...reasonContext
|
|
736
|
+
};
|
|
737
|
+
}
|
|
654
738
|
const counts = validateTerminalCountPayload({
|
|
655
739
|
userInputCountTotal: stored.total,
|
|
656
740
|
userInputCountAfterFlowStart: stored.after_flow_start,
|
|
@@ -659,19 +743,25 @@ export function terminalMetricContextFromArgs(args, context = {}) {
|
|
|
659
743
|
}, process.env);
|
|
660
744
|
if (!counts) return {};
|
|
661
745
|
return {
|
|
746
|
+
rootIntent: counts.rootIntent,
|
|
662
747
|
userInputCountTotal: counts.total,
|
|
663
748
|
userInputCountAfterFlowStart: counts.afterFlowStart,
|
|
664
|
-
flowStartObserved: counts.flowStartObserved
|
|
749
|
+
flowStartObserved: counts.flowStartObserved,
|
|
750
|
+
...reasonContext
|
|
665
751
|
};
|
|
666
752
|
}
|
|
667
753
|
|
|
668
754
|
function contextFromCliArgs(args) {
|
|
669
|
-
|
|
755
|
+
const context = {
|
|
670
756
|
runId: args['run-id'],
|
|
671
757
|
rootIntent: args['root-intent'],
|
|
672
758
|
productType: args['product-type'],
|
|
673
|
-
productName: args['product-name']
|
|
674
|
-
|
|
759
|
+
productName: args['product-name']
|
|
760
|
+
};
|
|
761
|
+
if (args._[0] !== 'run.terminal') return context;
|
|
762
|
+
return {
|
|
763
|
+
...context,
|
|
764
|
+
...terminalMetricContextFromArgs(args, context)
|
|
675
765
|
};
|
|
676
766
|
}
|
|
677
767
|
|
|
@@ -699,11 +789,42 @@ function createRunState(args, env = process.env, nowMs = Date.now()) {
|
|
|
699
789
|
run_id: runId,
|
|
700
790
|
root_intent: rootIntent,
|
|
701
791
|
product_type: productType,
|
|
792
|
+
user_input_counts: initialUserInputCounts(nowMs, args['input-id'] || initialUserInputId, runId),
|
|
702
793
|
created_at: nowMs,
|
|
703
794
|
updated_at: nowMs
|
|
704
795
|
};
|
|
705
796
|
}
|
|
706
797
|
|
|
798
|
+
function withInitialUserInputCount(state, nowMs = Date.now(), inputId = initialUserInputId) {
|
|
799
|
+
const counts = normalizeUserInputCounts(state?.user_input_counts);
|
|
800
|
+
if (counts.total >= 1) return state;
|
|
801
|
+
return {
|
|
802
|
+
...state,
|
|
803
|
+
user_input_counts: initialUserInputCounts(nowMs, inputId, state.run_id),
|
|
804
|
+
updated_at: nowMs
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function withCurrentUserInputCount(state, nowMs = Date.now(), inputId = '') {
|
|
809
|
+
const normalizedInputId = normalizeUserInputId(inputId);
|
|
810
|
+
const counts = normalizeUserInputCounts(state?.user_input_counts);
|
|
811
|
+
if (counts.total < 1) return withInitialUserInputCount(state, nowMs, normalizedInputId || initialUserInputId);
|
|
812
|
+
if (!normalizedInputId) return state;
|
|
813
|
+
const nextCounts = addUserInputCount(
|
|
814
|
+
counts,
|
|
815
|
+
counts.flow_start_observed ? 'after-flow' : 'before-flow',
|
|
816
|
+
normalizedInputId,
|
|
817
|
+
state.run_id,
|
|
818
|
+
nowMs
|
|
819
|
+
);
|
|
820
|
+
if (nextCounts === counts) return state;
|
|
821
|
+
return {
|
|
822
|
+
...state,
|
|
823
|
+
user_input_counts: nextCounts,
|
|
824
|
+
updated_at: nowMs
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
707
828
|
function compatibleProductType(existingProductType, requestedProductType) {
|
|
708
829
|
const existing = canonicalProductType(existingProductType) || 'UNKNOWN';
|
|
709
830
|
const requested = canonicalProductType(requestedProductType) || 'UNKNOWN';
|
|
@@ -740,12 +861,12 @@ function createOrReuseRunState(args, env = process.env, nowMs = Date.now()) {
|
|
|
740
861
|
const requestedRunId = platformProjectRunId(env) || safeRunId(args['run-id']);
|
|
741
862
|
const existing = readRunState(env);
|
|
742
863
|
if (reusableRunState(existing, rootIntent, requestedProductType) && (!requestedRunId || existing.run_id === requestedRunId)) {
|
|
743
|
-
return {
|
|
864
|
+
return withCurrentUserInputCount({
|
|
744
865
|
...existing,
|
|
745
866
|
root_intent: rootIntent,
|
|
746
867
|
product_type: mergeProductType(existing.product_type, requestedProductType),
|
|
747
868
|
updated_at: nowMs
|
|
748
|
-
};
|
|
869
|
+
}, nowMs, args['input-id']);
|
|
749
870
|
}
|
|
750
871
|
return createRunState(args, env, nowMs);
|
|
751
872
|
}
|
|
@@ -803,25 +924,26 @@ export function markTelemetryFlowStart(env = process.env, nowMs = Date.now()) {
|
|
|
803
924
|
return next.user_input_counts;
|
|
804
925
|
}
|
|
805
926
|
|
|
806
|
-
export function recordTelemetryUserInput(phase, env = process.env, nowMs = Date.now()) {
|
|
927
|
+
export function recordTelemetryUserInput(phase, env = process.env, nowMs = Date.now(), inputId = '') {
|
|
807
928
|
if (!telemetryEnabled(env)) return null;
|
|
808
929
|
if (!['before-flow', 'after-flow'].includes(phase)) {
|
|
809
930
|
throw new Error('--phase 仅支持 before-flow、after-flow');
|
|
810
931
|
}
|
|
932
|
+
const normalizedInputId = normalizeUserInputId(inputId);
|
|
933
|
+
if (!normalizedInputId) throw new Error('用户输入计数必须提供本地幂等 --input-id,且不得包含用户输入原文或敏感信息');
|
|
811
934
|
const existing = readRunState(env);
|
|
812
935
|
if (!existing?.run_id || existing.terminal) return null;
|
|
813
936
|
const counts = normalizeUserInputCounts(existing.user_input_counts);
|
|
814
937
|
if (phase === 'before-flow' && counts.flow_start_observed) {
|
|
815
938
|
throw new Error('当前 run 已观测 flow start,后续用户输入必须记录为 after-flow');
|
|
816
939
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (phase === 'after-flow' && !counts.flow_start_observed) nextCounts.flow_start_observed_at = nowMs;
|
|
940
|
+
if (phase === 'after-flow' && !counts.flow_start_observed) {
|
|
941
|
+
throw new Error('当前 run 尚未观测 flow start,不能记录 after-flow 用户输入');
|
|
942
|
+
}
|
|
943
|
+
const inputHash = userInputHash(normalizedInputId, existing.run_id);
|
|
944
|
+
if (!inputHash) throw new Error('用户输入计数 input-id 无法生成本地幂等键');
|
|
945
|
+
if (counts.input_hashes.includes(inputHash)) return counts;
|
|
946
|
+
const nextCounts = addUserInputCount(counts, phase, normalizedInputId, existing.run_id, nowMs);
|
|
825
947
|
const next = {
|
|
826
948
|
...existing,
|
|
827
949
|
user_input_counts: nextCounts,
|
|
@@ -831,6 +953,14 @@ export function recordTelemetryUserInput(phase, env = process.env, nowMs = Date.
|
|
|
831
953
|
return nextCounts;
|
|
832
954
|
}
|
|
833
955
|
|
|
956
|
+
export function recordTelemetryUserInputForCurrentPhase(inputId, env = process.env, nowMs = Date.now()) {
|
|
957
|
+
if (!telemetryEnabled(env)) return null;
|
|
958
|
+
const existing = readRunState(env);
|
|
959
|
+
if (!existing?.run_id || existing.terminal) return null;
|
|
960
|
+
const counts = normalizeUserInputCounts(existing.user_input_counts);
|
|
961
|
+
return recordTelemetryUserInput(counts.flow_start_observed ? 'after-flow' : 'before-flow', env, nowMs, inputId);
|
|
962
|
+
}
|
|
963
|
+
|
|
834
964
|
function updateExistingRunStateFromEvent(event, env = process.env) {
|
|
835
965
|
const existing = readRunState(env);
|
|
836
966
|
if (!existing) return false;
|
|
@@ -1280,22 +1410,6 @@ function readUploadStatus(env = process.env) {
|
|
|
1280
1410
|
}
|
|
1281
1411
|
}
|
|
1282
1412
|
|
|
1283
|
-
function readPermissionedFlushState(env = process.env, nowMs = Date.now()) {
|
|
1284
|
-
try {
|
|
1285
|
-
const parsed = JSON.parse(fs.readFileSync(permissionedFlushStatePath(env), 'utf8'));
|
|
1286
|
-
if (!parsed || parsed.schema_ver !== 1) return null;
|
|
1287
|
-
const requestedAt = Number(parsed.requested_at || 0);
|
|
1288
|
-
if (!Number.isFinite(requestedAt) || nowMs - requestedAt > permissionedFlushTtlMs) return null;
|
|
1289
|
-
return parsed;
|
|
1290
|
-
} catch {
|
|
1291
|
-
return null;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
function writePermissionedFlushState(state, env = process.env) {
|
|
1296
|
-
return writePrivateJson(permissionedFlushStatePath(env), { schema_ver: 1, ...state });
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
1413
|
function networkUploadFailure(status) {
|
|
1300
1414
|
const errorCode = safeDimension(status?.error_code || '', '', 80).toUpperCase();
|
|
1301
1415
|
if (permissionedFlushNetworkErrors.has(errorCode)) return errorCode;
|
|
@@ -1309,49 +1423,41 @@ function networkUploadFailure(status) {
|
|
|
1309
1423
|
return '';
|
|
1310
1424
|
}
|
|
1311
1425
|
|
|
1312
|
-
export function permissionedFlushDecision(env = process.env
|
|
1426
|
+
export function permissionedFlushDecision(env = process.env) {
|
|
1313
1427
|
const outboxCount = outboxFiles(env).length;
|
|
1314
1428
|
const status = readUploadStatus(env);
|
|
1315
1429
|
const errorCode = networkUploadFailure(status);
|
|
1316
|
-
const existing = readPermissionedFlushState(env, nowMs);
|
|
1317
|
-
const alreadyRequested = Boolean(existing);
|
|
1318
1430
|
const result = {
|
|
1319
1431
|
needed: false,
|
|
1320
1432
|
outbox_count: outboxCount,
|
|
1321
|
-
already_requested: alreadyRequested,
|
|
1322
1433
|
error_code: errorCode || safeDimension(status?.error_code || '', '', 80),
|
|
1323
1434
|
status: safeDimension(status?.status || 'unknown', 'unknown', 80)
|
|
1324
1435
|
};
|
|
1325
1436
|
if (!telemetryUploadEnabled(env) || outboxCount <= 0 || !errorCode) {
|
|
1326
1437
|
return result;
|
|
1327
1438
|
}
|
|
1328
|
-
if (alreadyRequested) {
|
|
1329
|
-
return {
|
|
1330
|
-
...result,
|
|
1331
|
-
requested_at: existing.requested_at
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
1439
|
result.needed = true;
|
|
1335
|
-
if (markRequested) {
|
|
1336
|
-
writePermissionedFlushState({
|
|
1337
|
-
requested_at: nowMs,
|
|
1338
|
-
error_code: errorCode,
|
|
1339
|
-
outbox_count: outboxCount
|
|
1340
|
-
}, env);
|
|
1341
|
-
result.requested_at = nowMs;
|
|
1342
|
-
}
|
|
1343
1440
|
return result;
|
|
1344
1441
|
}
|
|
1345
1442
|
|
|
1443
|
+
function emitPermissionedFlushNeededMarkers(env = process.env, stream = process.stderr) {
|
|
1444
|
+
const decision = permissionedFlushDecision(env);
|
|
1445
|
+
if (!decision.needed || !stream || typeof stream.write !== 'function') return false;
|
|
1446
|
+
stream.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
|
|
1447
|
+
stream.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
|
|
1448
|
+
return true;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1346
1451
|
export async function flushOutbox(env = process.env, {
|
|
1347
1452
|
limit = outboxFlushLimit,
|
|
1348
1453
|
targetFile = '',
|
|
1349
1454
|
timeoutMs = uploadTimeoutMs,
|
|
1350
|
-
sender = sendSyncLogReportDetailed
|
|
1455
|
+
sender = sendSyncLogReportDetailed,
|
|
1456
|
+
prioritizeTerminal = false
|
|
1351
1457
|
} = {}) {
|
|
1352
1458
|
const result = { attempted: 0, sent: 0, retained: 0, invalid: 0 };
|
|
1353
1459
|
if (!telemetryUploadEnabled(env)) return result;
|
|
1354
|
-
const files =
|
|
1460
|
+
const files = outboxFilesForFlush(env, { targetFile, limit, prioritizeTerminal });
|
|
1355
1461
|
for (const file of files) {
|
|
1356
1462
|
const claimedFile = claimOutboxFile(file);
|
|
1357
1463
|
if (!claimedFile) continue;
|
|
@@ -1413,6 +1519,26 @@ export async function flushOutbox(env = process.env, {
|
|
|
1413
1519
|
return result;
|
|
1414
1520
|
}
|
|
1415
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
|
+
|
|
1416
1542
|
function spawnOutboxFlush(env = process.env, { delayMs = 0 } = {}) {
|
|
1417
1543
|
if (!telemetryUploadEnabled(env)) return false;
|
|
1418
1544
|
try {
|
|
@@ -1445,6 +1571,34 @@ function spawnOutboxFlush(env = process.env, { delayMs = 0 } = {}) {
|
|
|
1445
1571
|
}
|
|
1446
1572
|
}
|
|
1447
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
|
+
|
|
1448
1602
|
function foregroundFlushTerminalOutboxFile(file, env = process.env) {
|
|
1449
1603
|
if (!telemetryUploadEnabled(env) || !file) return false;
|
|
1450
1604
|
const parsed = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_TERMINAL_FLUSH_TIMEOUT_MS);
|
|
@@ -1487,8 +1641,13 @@ function scheduleSyncLogUpload(event, env = process.env) {
|
|
|
1487
1641
|
return false;
|
|
1488
1642
|
}
|
|
1489
1643
|
if (event?.stage === 'run.terminal') {
|
|
1490
|
-
if (
|
|
1644
|
+
if (foregroundFlushTerminalOutboxFile(queuedFile, env)) {
|
|
1645
|
+
if (outboxFiles(env).length > 0) {
|
|
1646
|
+
spawnOutboxFlush(env, { delayMs: terminalBackgroundRetryDelayMs });
|
|
1647
|
+
}
|
|
1648
|
+
} else {
|
|
1491
1649
|
spawnOutboxFlush(env, { delayMs: terminalBackgroundRetryDelayMs });
|
|
1650
|
+
emitPermissionedFlushNeededMarkers(env);
|
|
1492
1651
|
}
|
|
1493
1652
|
return true;
|
|
1494
1653
|
}
|
|
@@ -1507,7 +1666,7 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1507
1666
|
try {
|
|
1508
1667
|
const { command, args } = parseCliArgs(argv);
|
|
1509
1668
|
if (command === 'start-run') {
|
|
1510
|
-
assertCliArgs(args, new Set(['root-intent', 'product-type', 'run-id']), 0);
|
|
1669
|
+
assertCliArgs(args, new Set(['root-intent', 'product-type', 'run-id', 'input-id']), 0);
|
|
1511
1670
|
const state = createOrReuseRunState(args);
|
|
1512
1671
|
if (!writeRunState(state) && telemetryEnabled(process.env)) {
|
|
1513
1672
|
recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to write local telemetry run state');
|
|
@@ -1533,10 +1692,10 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1533
1692
|
return 0;
|
|
1534
1693
|
}
|
|
1535
1694
|
if (command === 'count-user-input' && args._.length === 0) {
|
|
1536
|
-
assertCliArgs(args, new Set(['phase']), 0);
|
|
1537
|
-
const counts = recordTelemetryUserInput(args.phase, process.env, Date.now());
|
|
1695
|
+
assertCliArgs(args, new Set(['phase', 'input-id']), 0);
|
|
1696
|
+
const counts = recordTelemetryUserInput(args.phase, process.env, Date.now(), args['input-id']);
|
|
1538
1697
|
if (counts) {
|
|
1539
|
-
process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE=${JSON.stringify(counts)}\n`);
|
|
1698
|
+
process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE=${JSON.stringify(publicUserInputCountState(counts))}\n`);
|
|
1540
1699
|
} else {
|
|
1541
1700
|
process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE_MISSING=true\n`);
|
|
1542
1701
|
}
|
|
@@ -1546,7 +1705,7 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1546
1705
|
assertCliArgs(args, new Set(), 0);
|
|
1547
1706
|
const counts = markTelemetryFlowStart(process.env, Date.now());
|
|
1548
1707
|
if (counts) {
|
|
1549
|
-
process.stderr.write(`${internalPrefix}USER_INPUT_FLOW_START_STATE=${JSON.stringify(counts)}\n`);
|
|
1708
|
+
process.stderr.write(`${internalPrefix}USER_INPUT_FLOW_START_STATE=${JSON.stringify(publicUserInputCountState(counts))}\n`);
|
|
1550
1709
|
} else {
|
|
1551
1710
|
process.stderr.write(`${internalPrefix}USER_INPUT_FLOW_START_STATE_MISSING=true\n`);
|
|
1552
1711
|
}
|
|
@@ -1560,7 +1719,7 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1560
1719
|
}
|
|
1561
1720
|
if (command === 'permissioned-flush-status' && args._.length === 0) {
|
|
1562
1721
|
assertCliArgs(args, new Set(), 0);
|
|
1563
|
-
const decision = permissionedFlushDecision(process.env
|
|
1722
|
+
const decision = permissionedFlushDecision(process.env);
|
|
1564
1723
|
process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_STATUS=${JSON.stringify(decision)}\n`);
|
|
1565
1724
|
if (decision.needed) {
|
|
1566
1725
|
process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_CANDIDATE=true\n`);
|
|
@@ -1569,17 +1728,15 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1569
1728
|
}
|
|
1570
1729
|
if (command === 'permissioned-flush-needed' && args._.length === 0) {
|
|
1571
1730
|
assertCliArgs(args, new Set(), 0);
|
|
1572
|
-
const decision = permissionedFlushDecision(process.env
|
|
1731
|
+
const decision = permissionedFlushDecision(process.env);
|
|
1573
1732
|
process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
|
|
1574
|
-
if (decision.needed) {
|
|
1575
|
-
process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
|
|
1576
|
-
}
|
|
1733
|
+
if (decision.needed) process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
|
|
1577
1734
|
return 0;
|
|
1578
1735
|
}
|
|
1579
1736
|
if (command === 'flush-outbox') {
|
|
1580
1737
|
assertCliArgs(args, new Set(), 0);
|
|
1581
1738
|
if (!telemetryEnabled(process.env)) return 0;
|
|
1582
|
-
|
|
1739
|
+
await runPermissionedOutboxFlush(process.env, { spawnBackground: true });
|
|
1583
1740
|
return 0;
|
|
1584
1741
|
}
|
|
1585
1742
|
if (command === 'flush-outbox-worker') {
|
|
@@ -1604,6 +1761,7 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1604
1761
|
'root-intent',
|
|
1605
1762
|
'product-type',
|
|
1606
1763
|
'product-name',
|
|
1764
|
+
'terminal-reason',
|
|
1607
1765
|
'timestamp-ms',
|
|
1608
1766
|
'user-input-count-total',
|
|
1609
1767
|
'user-input-count-after-flow-start',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
-
|
|
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
|
-
-
|
|
4
|
-
- 补传:不为权限探测新增网络事件;实际低敏上传就是自然探测,宿主已允许访问 collector
|
|
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` 兜底。
|
|
@@ -56,7 +56,7 @@ 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
62
|
node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry
|
|
@@ -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
|
|
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
|
|
|
@@ -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
|
|
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` | 当前阶段的授权、查询或写分支模块 |
|
|
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` 关闭事件/outbox/上传/flush;私有 run context 仍供 `PLATFORM_ID` 与终态生命周期。失败静默,不改变业务/消息/确认,stdout 空且 stderr 内部行不对客。`integration_only` 由 checklist 终结;Onboarding 由 closeout
|
|
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
|
+
|
|
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`。`terminal_reason` 为终态 runner 写入的固定低敏枚举,禁自由文本、错误详情和业务响应。不新增业务确认点或独立上报;入队后当前 `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
|
|