@alipay/alipay-aipay 1.6.2-beta → 1.6.2-beta3
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 +9 -13
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +6 -4
- package/skill/references/normal/scripts/flow_loader.mjs +4 -1
- package/skill/references/normal/scripts/integration_message_runner.mjs +5 -1
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +5 -1
- package/skill/references/normal/scripts/telemetry.mjs +357 -171
- package/skill/references/normal/telemetry.md +4 -5
- package/skill/references/onboarding/flow.md +6 -4
- package/skill/references/runtime-entry.md +2 -2
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -5,27 +5,23 @@ description: >-
|
|
|
5
5
|
触发关键词:"AI支付"、"接支付宝"、"一站式接支付宝"、"接个支付"、"开发支付"、"402收款"、"AI按量付费"、"AI网页应用收款"、"AI移动应用收款"、"按量付费"、"快捷收款"、"快捷收单"、"网站支付"、"APP 支付"、"产品开通"、"商家开通"、"签约入驻"等。
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
本文件只负责在加载业务规则前确定本轮实际使用的 Skill 版本。禁止在完成下列启动协议前读取 `references/runtime-entry.md`、flow、模块、消息目录或业务脚本。
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## 前置版本检查
|
|
13
|
-
|
|
14
|
-
本次 Agent 对话首次使用 `alipay-aipay` 时,先把本文件所在目录解析为规范化绝对路径 `<SKILL_DIR>`,记录一次性事实 `ALIPAY_AIPAY_SELF_UPDATE_CHECKED=true`,再执行以下单条命令。`<SKILL_DIR>` 是执行时替换的路径占位符;禁止用 shell 变量赋值、命令替换或依赖特定 shell:
|
|
10
|
+
首次使用 `alipay-aipay`:解析本文件目录为规范化绝对 `<SKILL_DIR>`,记录 `ALIPAY_AIPAY_SELF_UPDATE_CHECKED=true`,执行单命令;`<SKILL_DIR>` 是占位符,禁止用 shell 变量赋值、命令替换或依赖特定 shell:
|
|
15
11
|
|
|
16
12
|
```text
|
|
17
13
|
node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" self-update check --skill-dir "<SKILL_DIR>"
|
|
18
14
|
```
|
|
19
15
|
|
|
20
|
-
`SKILL_DIR`
|
|
16
|
+
`SKILL_DIR` 只绑定当前 Skill,禁止换成 Agent 启动目录、用户项目、Home、npm 源码根或其他副本。`VERSION` 缺失/非法或入口缺失由命令修复;失败或 marker 不唯一按 `SELF_UPDATE:CHECK_FAILED`。
|
|
21
17
|
|
|
22
|
-
stderr
|
|
18
|
+
stderr 只允许一个内部终态,禁止展示 marker 或 npm/npx 原始输出:
|
|
23
19
|
|
|
24
|
-
- `SELF_UPDATE:UPDATED=<合法 SemVer
|
|
20
|
+
- `SELF_UPDATE:UPDATED=<合法 SemVer>`:已切换新版。
|
|
25
21
|
- `SELF_UPDATE:CURRENT=<合法 SemVer>`:继续当前版本。
|
|
26
|
-
- `SELF_UPDATE:RETRY_WITH_NETWORK
|
|
27
|
-
- `SELF_UPDATE:SKIPPED_NETWORK_RESTRICTED|CHECK_FAILED|INSTALL_FAILED|VERIFY_FAILED
|
|
22
|
+
- `SELF_UPDATE:RETRY_WITH_NETWORK`:可取权后原样重试一次。
|
|
23
|
+
- `SELF_UPDATE:SKIPPED_NETWORK_RESTRICTED|CHECK_FAILED|INSTALL_FAILED|VERIFY_FAILED`:继续当前可用版本。
|
|
28
24
|
|
|
29
|
-
除 `RETRY_WITH_NETWORK`
|
|
25
|
+
除 `RETRY_WITH_NETWORK` 外,只完整读取同一 `SKILL_DIR` 的 `references/runtime-entry.md`,记录 `ALIPAY_AIPAY_RUNTIME_ENTRY_LOADED=true` 和路径,再路由。`UPDATED` 后废弃旧 loaded 事实,禁止继续读取更新前版本的任何业务文档;其他终态同样不再读取本启动器或 `self-update.md` 补充规则。
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
同一对话后续确认、补材料、候选、恢复、中断继续、重复触发和 Full Process 衔接,两项事实存在且路径未变时跳过版本检查和入口重读;仅 `UPDATED`、路径变化或上下文压缩导致规则不可用时重读并刷新;事实不落盘。
|
package/skill/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.6.2-
|
|
1
|
+
1.6.2-beta3
|
|
@@ -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
|
|
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"
|
|
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"
|
|
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"
|
|
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({
|
|
1360
|
+
emitTerminalMetric({
|
|
1361
|
+
rootIntent: 'integration_only',
|
|
1362
|
+
productType: variant,
|
|
1363
|
+
...terminalMetricContextFromArgs(args, { rootIntent: 'integration_only' })
|
|
1364
|
+
});
|
|
1361
1365
|
}
|
|
1362
1366
|
return rendered;
|
|
1363
1367
|
}
|
|
@@ -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({
|
|
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
|
|
|
@@ -18,12 +18,35 @@ const syncLogCode = '1005';
|
|
|
18
18
|
const syncLogMsg = 'alipay_aipay_skill_event';
|
|
19
19
|
const syncLogYuyanID = '180020010001290755';
|
|
20
20
|
const uploadTimeoutMs = 2500;
|
|
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
|
+
]);
|
|
21
30
|
const outboxFlushLimit = 50;
|
|
22
31
|
const outboxClaimTtlMs = 5 * 60 * 1000;
|
|
23
32
|
const outboxMaxRecords = 1000;
|
|
24
33
|
const outboxMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
|
|
25
34
|
const permissionedFlushTtlMs = 12 * 60 * 60 * 1000;
|
|
26
35
|
const platformProjectIdEnvKeys = Object.freeze(['COZE_PROJECT_ID', 'MEOO_PROJECT_ID']);
|
|
36
|
+
const sessionStateEnvKeys = Object.freeze([
|
|
37
|
+
'CODEX_THREAD_ID',
|
|
38
|
+
'CODEX_SESSION_ID',
|
|
39
|
+
'CLAUDE_CODE_SESSION_ID',
|
|
40
|
+
'OPENCLAW_SESSION_ID',
|
|
41
|
+
'CURSOR_SESSION_ID',
|
|
42
|
+
'CURSOR_TRACE_ID',
|
|
43
|
+
'COZE_SESSION_ID',
|
|
44
|
+
'COZE_CONVERSATION_ID',
|
|
45
|
+
'HERMES_SESSION_ID',
|
|
46
|
+
'OPENAI_CONVERSATION_ID',
|
|
47
|
+
'SESSION_ID',
|
|
48
|
+
'CI_JOB_ID'
|
|
49
|
+
]);
|
|
27
50
|
const permissionedFlushNetworkErrors = new Set([
|
|
28
51
|
'ENOTFOUND',
|
|
29
52
|
'EAI_AGAIN',
|
|
@@ -119,6 +142,14 @@ function safeDimension(value, fallback = 'unknown', maxLength = 80) {
|
|
|
119
142
|
return trimmed.slice(0, maxLength);
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
function failedResult(error, code = 'REQUEST_ERROR', message = 'request error') {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error_code: safeDimension(error?.code || error?.name || code, code, 80),
|
|
149
|
+
error_message: safeDimension(error?.message || message, message, 240)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
function normalizeSwitch(value) {
|
|
123
154
|
return String(value || '').trim().toLowerCase();
|
|
124
155
|
}
|
|
@@ -161,28 +192,20 @@ function isMaintenanceProcess(env = process.env, argv = process.argv) {
|
|
|
161
192
|
return argv.some((arg) => /(^|[/\\])test[/\\].+\.test\.mjs$/u.test(String(arg || '')));
|
|
162
193
|
}
|
|
163
194
|
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
env.CODEX_SESSION_ID ||
|
|
170
|
-
env.CLAUDE_CODE_SESSION_ID ||
|
|
171
|
-
env.OPENCLAW_SESSION_ID ||
|
|
172
|
-
env.CURSOR_SESSION_ID ||
|
|
173
|
-
env.CURSOR_TRACE_ID ||
|
|
174
|
-
env.COZE_SESSION_ID ||
|
|
175
|
-
env.COZE_CONVERSATION_ID ||
|
|
176
|
-
env.HERMES_SESSION_ID ||
|
|
177
|
-
env.OPENAI_CONVERSATION_ID ||
|
|
178
|
-
env.SESSION_ID ||
|
|
179
|
-
env.CI_JOB_ID ||
|
|
180
|
-
'';
|
|
195
|
+
function stableStateSource(env = process.env) {
|
|
196
|
+
return env.ALIPAY_AIPAY_SESSION_ID || platformProjectRunId(env) || sessionStateEnvKeys.map((key) => env[key]).find(Boolean) || '';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function stateKeyFromSource(source) {
|
|
181
200
|
if (!source) return 'default';
|
|
182
201
|
if (safeText(source) && source.length <= 128) return source;
|
|
183
202
|
return `key-${crypto.createHash('sha256').update(source).digest('hex')}`;
|
|
184
203
|
}
|
|
185
204
|
|
|
205
|
+
function stableStateKey(env = process.env) {
|
|
206
|
+
return stateKeyFromSource(stableStateSource(env));
|
|
207
|
+
}
|
|
208
|
+
|
|
186
209
|
function stateDir(env = process.env) {
|
|
187
210
|
if (env.ALIPAY_AIPAY_TELEMETRY_DIR) return env.ALIPAY_AIPAY_TELEMETRY_DIR;
|
|
188
211
|
return telemetryRoot();
|
|
@@ -207,7 +230,8 @@ function permissionedFlushStatePath(env = process.env) {
|
|
|
207
230
|
if (env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE) {
|
|
208
231
|
return env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE;
|
|
209
232
|
}
|
|
210
|
-
|
|
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`);
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
export function telemetryStoragePaths(env = process.env) {
|
|
@@ -224,8 +248,21 @@ function ensurePrivateDir(dir) {
|
|
|
224
248
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
225
249
|
try {
|
|
226
250
|
fs.chmodSync(dir, 0o700);
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function writePrivateJson(file, value) {
|
|
255
|
+
try {
|
|
256
|
+
ensurePrivateDir(path.dirname(file));
|
|
257
|
+
const tempPath = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.tmp`);
|
|
258
|
+
fs.writeFileSync(tempPath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
259
|
+
try {
|
|
260
|
+
fs.chmodSync(tempPath, 0o600);
|
|
261
|
+
} catch {}
|
|
262
|
+
fs.renameSync(tempPath, file);
|
|
263
|
+
return true;
|
|
227
264
|
} catch {
|
|
228
|
-
|
|
265
|
+
return false;
|
|
229
266
|
}
|
|
230
267
|
}
|
|
231
268
|
|
|
@@ -273,21 +310,7 @@ function recordLocalTelemetryFailure(errorCode, message, env = process.env) {
|
|
|
273
310
|
}
|
|
274
311
|
|
|
275
312
|
function writeRunState(state, env = process.env) {
|
|
276
|
-
|
|
277
|
-
const statePath = runStatePath(env);
|
|
278
|
-
ensurePrivateDir(path.dirname(statePath));
|
|
279
|
-
const tempPath = path.join(path.dirname(statePath), `.${path.basename(statePath)}.${process.pid}.tmp`);
|
|
280
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
|
|
281
|
-
try {
|
|
282
|
-
fs.chmodSync(tempPath, 0o600);
|
|
283
|
-
} catch {
|
|
284
|
-
// chmod is best-effort on Windows.
|
|
285
|
-
}
|
|
286
|
-
fs.renameSync(tempPath, statePath);
|
|
287
|
-
return true;
|
|
288
|
-
} catch {
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
313
|
+
return writePrivateJson(runStatePath(env), state);
|
|
291
314
|
}
|
|
292
315
|
|
|
293
316
|
function outboxFilename(event) {
|
|
@@ -296,36 +319,31 @@ function outboxFilename(event) {
|
|
|
296
319
|
return `${timestamp}-${eventID}.json`;
|
|
297
320
|
}
|
|
298
321
|
|
|
299
|
-
|
|
322
|
+
function writeOutboxRecord(event, env = process.env) {
|
|
300
323
|
try {
|
|
301
|
-
if (!event) return
|
|
324
|
+
if (!event) return '';
|
|
302
325
|
const dir = outboxDir(env);
|
|
303
326
|
ensurePrivateDir(dir);
|
|
304
327
|
const filename = outboxFilename(event);
|
|
305
328
|
const finalPath = path.join(dir, filename);
|
|
306
|
-
|
|
307
|
-
const record = {
|
|
329
|
+
if (!writePrivateJson(finalPath, {
|
|
308
330
|
schema_ver: 1,
|
|
309
331
|
created_at: Date.now(),
|
|
310
332
|
event
|
|
311
|
-
};
|
|
312
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
313
|
-
try {
|
|
314
|
-
fs.chmodSync(tempPath, 0o600);
|
|
315
|
-
} catch {
|
|
316
|
-
// chmod is best-effort on Windows.
|
|
317
|
-
}
|
|
318
|
-
fs.renameSync(tempPath, finalPath);
|
|
333
|
+
})) return '';
|
|
319
334
|
pruneOutbox(env);
|
|
320
|
-
return
|
|
335
|
+
return finalPath;
|
|
321
336
|
} catch {
|
|
322
|
-
return
|
|
337
|
+
return '';
|
|
323
338
|
}
|
|
324
339
|
}
|
|
325
340
|
|
|
341
|
+
export function enqueueMetricEvent(event, env = process.env) {
|
|
342
|
+
return Boolean(writeOutboxRecord(event, env));
|
|
343
|
+
}
|
|
344
|
+
|
|
326
345
|
function safeRunId(value) {
|
|
327
346
|
if (typeof value !== 'string' || !value.trim()) return '';
|
|
328
|
-
// Run IDs also participate in state keys and param4 matching, so keep them separator-free.
|
|
329
347
|
return /[\u0000-\u001f\u007f,^=]/u.test(value) ? '' : value;
|
|
330
348
|
}
|
|
331
349
|
|
|
@@ -382,6 +400,13 @@ function parseNonNegativeInteger(value) {
|
|
|
382
400
|
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
383
401
|
}
|
|
384
402
|
|
|
403
|
+
function sleepSync(ms) {
|
|
404
|
+
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
405
|
+
try {
|
|
406
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(ms, 30000));
|
|
407
|
+
} catch {}
|
|
408
|
+
}
|
|
409
|
+
|
|
385
410
|
function parseBooleanFlag(value) {
|
|
386
411
|
if (typeof value === 'boolean') return value;
|
|
387
412
|
if (typeof value !== 'string') return null;
|
|
@@ -391,23 +416,60 @@ function parseBooleanFlag(value) {
|
|
|
391
416
|
return null;
|
|
392
417
|
}
|
|
393
418
|
|
|
394
|
-
function
|
|
395
|
-
if (!
|
|
419
|
+
function normalizeUserInputCounts(value) {
|
|
420
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
421
|
+
return { total: 0, after_flow_start: 0, flow_start_observed: false };
|
|
422
|
+
}
|
|
423
|
+
const total = parseNonNegativeInteger(value.total ?? value.user_input_count_total);
|
|
424
|
+
const afterFlowStart = parseNonNegativeInteger(value.after_flow_start ?? value.user_input_count_after_flow_start);
|
|
425
|
+
const flowStartObserved = parseBooleanFlag(value.flow_start_observed);
|
|
426
|
+
if (total === null || afterFlowStart === null || flowStartObserved === null) {
|
|
427
|
+
return { total: 0, after_flow_start: 0, flow_start_observed: false };
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
total,
|
|
431
|
+
after_flow_start: Math.min(afterFlowStart, total),
|
|
432
|
+
flow_start_observed: flowStartObserved,
|
|
433
|
+
flow_start_observed_at: parseNonNegativeInteger(value.flow_start_observed_at) || undefined,
|
|
434
|
+
updated_at: parseNonNegativeInteger(value.updated_at) || undefined
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function validateTerminalCountPayload(payload = {}, env = process.env, { strict = false } = {}) {
|
|
439
|
+
const fail = (message) => {
|
|
440
|
+
if (strict) throw new Error(message);
|
|
441
|
+
return null;
|
|
442
|
+
};
|
|
396
443
|
const total = parseNonNegativeInteger(payload.userInputCountTotal ?? payload.user_input_count_total);
|
|
397
444
|
const afterFlowStart = parseNonNegativeInteger(
|
|
398
445
|
payload.userInputCountAfterFlowStart ?? payload.user_input_count_after_flow_start
|
|
399
446
|
);
|
|
400
447
|
const flowStartObserved = parseBooleanFlag(payload.flowStartObserved ?? payload.flow_start_observed);
|
|
401
448
|
const rootIntent = rootIntentFrom(payload, env);
|
|
402
|
-
if (total === null || afterFlowStart === null || flowStartObserved === null)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (
|
|
449
|
+
if (total === null || afterFlowStart === null || flowStartObserved === null) {
|
|
450
|
+
return fail('终态用户输入计数必须是合法非负整数,flow-start-observed 必须是 true/false');
|
|
451
|
+
}
|
|
452
|
+
if (total < 1) return fail('终态 user_input_count_total 必须至少包含触发当前 run 的用户输入');
|
|
453
|
+
if (!rootIntent) return fail('终态用户输入计数缺少合法 root_intent');
|
|
454
|
+
if (afterFlowStart > total) return fail('终态 user_input_count_after_flow_start 不能大于 user_input_count_total');
|
|
455
|
+
if (flowStartObserved && afterFlowStart >= total) {
|
|
456
|
+
return fail('终态 flow start 已观测时,after-flow 用户输入数必须小于 total;请重新核对 before-flow/after-flow 分界');
|
|
457
|
+
}
|
|
458
|
+
if (!flowStartObserved && afterFlowStart !== 0) {
|
|
459
|
+
return fail('终态 flow start 未观测时,after-flow 用户输入数必须为 0');
|
|
460
|
+
}
|
|
461
|
+
return { total, afterFlowStart, flowStartObserved, rootIntent };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function terminalExtInfoFromPayload(payload = {}, env = process.env) {
|
|
465
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return '';
|
|
466
|
+
const counts = validateTerminalCountPayload(payload, env);
|
|
467
|
+
if (!counts) return '';
|
|
406
468
|
return JSON.stringify({
|
|
407
|
-
user_input_count_total: total,
|
|
408
|
-
user_input_count_after_flow_start: afterFlowStart,
|
|
409
|
-
flow_start_observed: flowStartObserved,
|
|
410
|
-
root_intent: rootIntent
|
|
469
|
+
user_input_count_total: counts.total,
|
|
470
|
+
user_input_count_after_flow_start: counts.afterFlowStart,
|
|
471
|
+
flow_start_observed: counts.flowStartObserved,
|
|
472
|
+
root_intent: counts.rootIntent
|
|
411
473
|
});
|
|
412
474
|
}
|
|
413
475
|
|
|
@@ -441,6 +503,7 @@ export function stageMetricEvent(stage, context = {}, nowMs = Date.now(), env =
|
|
|
441
503
|
schema_ver: 2,
|
|
442
504
|
event_id: crypto.randomUUID(),
|
|
443
505
|
skill: 'alipay-aipay',
|
|
506
|
+
skill_version: skillVersionFromDisk(),
|
|
444
507
|
run_id: runId,
|
|
445
508
|
stage: rule.stage,
|
|
446
509
|
timestamp: nowMs,
|
|
@@ -564,15 +627,41 @@ function parseCliArgs(argv) {
|
|
|
564
627
|
return { command, args };
|
|
565
628
|
}
|
|
566
629
|
|
|
567
|
-
export function terminalMetricContextFromArgs(args) {
|
|
630
|
+
export function terminalMetricContextFromArgs(args, context = {}) {
|
|
568
631
|
const optional = (name) => {
|
|
569
632
|
const value = args[name];
|
|
570
633
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
571
634
|
};
|
|
635
|
+
const hasCountArg = terminalCountArgNames.some((name) => Object.prototype.hasOwnProperty.call(args, name));
|
|
636
|
+
if (hasCountArg) {
|
|
637
|
+
const missing = terminalCountArgNames.filter((name) => optional(name) === undefined);
|
|
638
|
+
if (missing.length > 0) throw new Error(`终态用户输入计数参数必须同时提供,缺少 --${missing[0]}`);
|
|
639
|
+
const counts = validateTerminalCountPayload({
|
|
640
|
+
userInputCountTotal: optional('user-input-count-total'),
|
|
641
|
+
userInputCountAfterFlowStart: optional('user-input-count-after-flow-start'),
|
|
642
|
+
flowStartObserved: optional('flow-start-observed'),
|
|
643
|
+
rootIntent: args['root-intent'] || args.rootIntent || args.root_intent || context.rootIntent || context.root_intent
|
|
644
|
+
}, process.env, { strict: true });
|
|
645
|
+
return {
|
|
646
|
+
userInputCountTotal: counts.total,
|
|
647
|
+
userInputCountAfterFlowStart: counts.afterFlowStart,
|
|
648
|
+
flowStartObserved: counts.flowStartObserved
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const state = readRunState(process.env);
|
|
652
|
+
const stored = normalizeUserInputCounts(state?.user_input_counts);
|
|
653
|
+
if (stored.total < 1) return {};
|
|
654
|
+
const counts = validateTerminalCountPayload({
|
|
655
|
+
userInputCountTotal: stored.total,
|
|
656
|
+
userInputCountAfterFlowStart: stored.after_flow_start,
|
|
657
|
+
flowStartObserved: stored.flow_start_observed,
|
|
658
|
+
rootIntent: state?.root_intent
|
|
659
|
+
}, process.env);
|
|
660
|
+
if (!counts) return {};
|
|
572
661
|
return {
|
|
573
|
-
userInputCountTotal:
|
|
574
|
-
userInputCountAfterFlowStart:
|
|
575
|
-
flowStartObserved:
|
|
662
|
+
userInputCountTotal: counts.total,
|
|
663
|
+
userInputCountAfterFlowStart: counts.afterFlowStart,
|
|
664
|
+
flowStartObserved: counts.flowStartObserved
|
|
576
665
|
};
|
|
577
666
|
}
|
|
578
667
|
|
|
@@ -661,9 +750,26 @@ function createOrReuseRunState(args, env = process.env, nowMs = Date.now()) {
|
|
|
661
750
|
return createRunState(args, env, nowMs);
|
|
662
751
|
}
|
|
663
752
|
|
|
753
|
+
function flowStartStage(stage) {
|
|
754
|
+
return stage === 'integration.user_confirmed' || stage === 'onboarding.user_confirmed';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function withFlowStartObserved(state, nowMs = Date.now()) {
|
|
758
|
+
const counts = normalizeUserInputCounts(state.user_input_counts);
|
|
759
|
+
return {
|
|
760
|
+
...state,
|
|
761
|
+
user_input_counts: {
|
|
762
|
+
...counts,
|
|
763
|
+
flow_start_observed: true,
|
|
764
|
+
flow_start_observed_at: counts.flow_start_observed_at || nowMs,
|
|
765
|
+
updated_at: nowMs
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
664
770
|
function updateRunStateFromEvent(event, env = process.env) {
|
|
665
771
|
const existing = readRunState(env) || {};
|
|
666
|
-
|
|
772
|
+
let next = {
|
|
667
773
|
...existing,
|
|
668
774
|
schema_ver: 1,
|
|
669
775
|
run_id: event.run_id,
|
|
@@ -672,6 +778,9 @@ function updateRunStateFromEvent(event, env = process.env) {
|
|
|
672
778
|
created_at: existing.created_at || Date.now(),
|
|
673
779
|
updated_at: Date.now()
|
|
674
780
|
};
|
|
781
|
+
if (flowStartStage(event.stage)) {
|
|
782
|
+
next = withFlowStartObserved(next, Number.isFinite(Number(event.timestamp)) ? Number(event.timestamp) : Date.now());
|
|
783
|
+
}
|
|
675
784
|
if (event.stage === 'run.terminal') {
|
|
676
785
|
next.terminal = true;
|
|
677
786
|
next.terminal_at = Number.isFinite(Number(event.timestamp)) ? Number(event.timestamp) : Date.now();
|
|
@@ -682,6 +791,46 @@ function updateRunStateFromEvent(event, env = process.env) {
|
|
|
682
791
|
writeRunState(next, env);
|
|
683
792
|
}
|
|
684
793
|
|
|
794
|
+
export function markTelemetryFlowStart(env = process.env, nowMs = Date.now()) {
|
|
795
|
+
if (!telemetryEnabled(env)) return null;
|
|
796
|
+
const existing = readRunState(env);
|
|
797
|
+
if (!existing?.run_id || existing.terminal) return null;
|
|
798
|
+
const next = withFlowStartObserved({
|
|
799
|
+
...existing,
|
|
800
|
+
updated_at: nowMs
|
|
801
|
+
}, nowMs);
|
|
802
|
+
writeRunState(next, env);
|
|
803
|
+
return next.user_input_counts;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
export function recordTelemetryUserInput(phase, env = process.env, nowMs = Date.now()) {
|
|
807
|
+
if (!telemetryEnabled(env)) return null;
|
|
808
|
+
if (!['before-flow', 'after-flow'].includes(phase)) {
|
|
809
|
+
throw new Error('--phase 仅支持 before-flow、after-flow');
|
|
810
|
+
}
|
|
811
|
+
const existing = readRunState(env);
|
|
812
|
+
if (!existing?.run_id || existing.terminal) return null;
|
|
813
|
+
const counts = normalizeUserInputCounts(existing.user_input_counts);
|
|
814
|
+
if (phase === 'before-flow' && counts.flow_start_observed) {
|
|
815
|
+
throw new Error('当前 run 已观测 flow start,后续用户输入必须记录为 after-flow');
|
|
816
|
+
}
|
|
817
|
+
const nextCounts = {
|
|
818
|
+
...counts,
|
|
819
|
+
total: counts.total + 1,
|
|
820
|
+
after_flow_start: counts.after_flow_start + (phase === 'after-flow' ? 1 : 0),
|
|
821
|
+
flow_start_observed: counts.flow_start_observed || phase === 'after-flow',
|
|
822
|
+
updated_at: nowMs
|
|
823
|
+
};
|
|
824
|
+
if (phase === 'after-flow' && !counts.flow_start_observed) nextCounts.flow_start_observed_at = nowMs;
|
|
825
|
+
const next = {
|
|
826
|
+
...existing,
|
|
827
|
+
user_input_counts: nextCounts,
|
|
828
|
+
updated_at: nowMs
|
|
829
|
+
};
|
|
830
|
+
writeRunState(next, env);
|
|
831
|
+
return nextCounts;
|
|
832
|
+
}
|
|
833
|
+
|
|
685
834
|
function updateExistingRunStateFromEvent(event, env = process.env) {
|
|
686
835
|
const existing = readRunState(env);
|
|
687
836
|
if (!existing) return false;
|
|
@@ -745,15 +894,20 @@ function modelFromEnv(env = process.env) {
|
|
|
745
894
|
|
|
746
895
|
function skillVersionFromDisk() {
|
|
747
896
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
if (match) return `v${match[1]}`;
|
|
751
|
-
} catch {
|
|
752
|
-
// fall through
|
|
753
|
-
}
|
|
897
|
+
return normalizeSkillVersion(fs.readFileSync(path.join(skillRoot(), 'VERSION'), 'utf8')) || 'unknown';
|
|
898
|
+
} catch {}
|
|
754
899
|
return 'unknown';
|
|
755
900
|
}
|
|
756
901
|
|
|
902
|
+
function normalizeSkillVersion(value) {
|
|
903
|
+
const match = String(value || '').trim().match(/^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/);
|
|
904
|
+
return match ? `v${match[1]}` : '';
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function skillVersionFromEvent(event) {
|
|
908
|
+
return normalizeSkillVersion(event?.skill_version) || skillVersionFromDisk();
|
|
909
|
+
}
|
|
910
|
+
|
|
757
911
|
function isLocalSkillCheckout() {
|
|
758
912
|
try {
|
|
759
913
|
return fs.existsSync(path.join(skillRoot(), '../.git'));
|
|
@@ -792,15 +946,11 @@ function machineCodeRaw(env = process.env) {
|
|
|
792
946
|
const match = output.match(/MachineGuid\s+REG_SZ\s+([^\s\r\n]+)/);
|
|
793
947
|
if (match?.[1]) return match[1];
|
|
794
948
|
}
|
|
795
|
-
} catch {
|
|
796
|
-
// fall through
|
|
797
|
-
}
|
|
949
|
+
} catch {}
|
|
798
950
|
try {
|
|
799
951
|
const hostname = os.hostname();
|
|
800
952
|
if (hostname) return `hostname:${hostname}`;
|
|
801
|
-
} catch {
|
|
802
|
-
// fall through
|
|
803
|
-
}
|
|
953
|
+
} catch {}
|
|
804
954
|
return '';
|
|
805
955
|
}
|
|
806
956
|
|
|
@@ -811,7 +961,7 @@ function machineUserID(env = process.env) {
|
|
|
811
961
|
return `machine_${hash}`;
|
|
812
962
|
}
|
|
813
963
|
|
|
814
|
-
function monitorEnv(env = process.env) {
|
|
964
|
+
function monitorEnv(env = process.env, skillVersion = skillVersionFromDisk()) {
|
|
815
965
|
switch (normalizeSwitch(env.AIPAY_BUILD_ENV)) {
|
|
816
966
|
case 'dev':
|
|
817
967
|
return 'DEV';
|
|
@@ -822,7 +972,6 @@ function monitorEnv(env = process.env) {
|
|
|
822
972
|
default:
|
|
823
973
|
break;
|
|
824
974
|
}
|
|
825
|
-
const skillVersion = skillVersionFromDisk();
|
|
826
975
|
if (/^v?\d+\.\d+\.\d+-/.test(skillVersion)) return 'PRE';
|
|
827
976
|
if (skillVersion !== 'unknown' && !isLocalSkillCheckout()) return 'PROD';
|
|
828
977
|
return 'DEV';
|
|
@@ -830,7 +979,7 @@ function monitorEnv(env = process.env) {
|
|
|
830
979
|
|
|
831
980
|
export function buildSyncLogReport(event, env = process.env) {
|
|
832
981
|
if (!event) return null;
|
|
833
|
-
const skillVersion =
|
|
982
|
+
const skillVersion = skillVersionFromEvent(event);
|
|
834
983
|
const item = {
|
|
835
984
|
code: syncLogCode,
|
|
836
985
|
msg: syncLogMsg,
|
|
@@ -848,7 +997,7 @@ export function buildSyncLogReport(event, env = process.env) {
|
|
|
848
997
|
return {
|
|
849
998
|
item,
|
|
850
999
|
options: {
|
|
851
|
-
Env: monitorEnv(env),
|
|
1000
|
+
Env: monitorEnv(env, skillVersion),
|
|
852
1001
|
SprintID: skillVersion,
|
|
853
1002
|
UserID: machineUserID(env),
|
|
854
1003
|
YuyanID: syncLogYuyanID
|
|
@@ -971,8 +1120,8 @@ export function sendSyncLogReportDetailed(report, { timeoutMs = uploadTimeoutMs
|
|
|
971
1120
|
status_code: statusCode || undefined,
|
|
972
1121
|
error_code: ok ? undefined : statusCode > 0 ? `HTTP_${statusCode}` : 'NO_HTTP_STATUS',
|
|
973
1122
|
error_message: ok ? undefined : statusCode > 0
|
|
974
|
-
? `
|
|
975
|
-
: '
|
|
1123
|
+
? `http ${statusCode}`
|
|
1124
|
+
: 'no http status'
|
|
976
1125
|
});
|
|
977
1126
|
});
|
|
978
1127
|
});
|
|
@@ -980,18 +1129,10 @@ export function sendSyncLogReportDetailed(report, { timeoutMs = uploadTimeoutMs
|
|
|
980
1129
|
finish({ ok: false, error_code: 'ETIMEDOUT', error_message: 'request timeout' });
|
|
981
1130
|
req.destroy();
|
|
982
1131
|
});
|
|
983
|
-
req.on('error', (error) => finish(
|
|
984
|
-
ok: false,
|
|
985
|
-
error_code: safeDimension(error?.code || error?.name || 'REQUEST_ERROR', 'REQUEST_ERROR', 80),
|
|
986
|
-
error_message: safeDimension(error?.message || 'request error', 'request error', 240)
|
|
987
|
-
}));
|
|
1132
|
+
req.on('error', (error) => finish(failedResult(error)));
|
|
988
1133
|
req.end(body);
|
|
989
1134
|
} catch (error) {
|
|
990
|
-
finish(
|
|
991
|
-
ok: false,
|
|
992
|
-
error_code: safeDimension(error?.code || error?.name || 'REQUEST_ERROR', 'REQUEST_ERROR', 80),
|
|
993
|
-
error_message: safeDimension(error?.message || 'request error', 'request error', 240)
|
|
994
|
-
});
|
|
1135
|
+
finish(failedResult(error));
|
|
995
1136
|
}
|
|
996
1137
|
});
|
|
997
1138
|
}
|
|
@@ -1001,14 +1142,25 @@ export async function sendSyncLogReport(report, { timeoutMs = uploadTimeoutMs }
|
|
|
1001
1142
|
return result.ok;
|
|
1002
1143
|
}
|
|
1003
1144
|
|
|
1004
|
-
function outboxFiles(env = process.env) {
|
|
1145
|
+
function outboxFiles(env = process.env, { targetFile = '', limit = 0 } = {}) {
|
|
1005
1146
|
try {
|
|
1006
1147
|
const dir = outboxDir(env);
|
|
1007
1148
|
recoverStaleOutboxClaims(env);
|
|
1008
1149
|
pruneOutbox(env);
|
|
1009
|
-
|
|
1150
|
+
if (targetFile) {
|
|
1151
|
+
const resolved = path.resolve(String(targetFile));
|
|
1152
|
+
const basename = path.basename(resolved);
|
|
1153
|
+
return path.dirname(resolved) === path.resolve(dir)
|
|
1154
|
+
&& basename.endsWith('.json')
|
|
1155
|
+
&& !basename.startsWith('.')
|
|
1156
|
+
&& fs.existsSync(resolved)
|
|
1157
|
+
? [resolved]
|
|
1158
|
+
: [];
|
|
1159
|
+
}
|
|
1160
|
+
const files = outboxRecordEntries(dir)
|
|
1010
1161
|
.sort((left, right) => left.name.localeCompare(right.name))
|
|
1011
1162
|
.map((entry) => entry.file);
|
|
1163
|
+
return limit > 0 ? files.slice(0, limit) : files;
|
|
1012
1164
|
} catch {
|
|
1013
1165
|
return [];
|
|
1014
1166
|
}
|
|
@@ -1022,9 +1174,7 @@ function outboxRecordEntries(dir) {
|
|
|
1022
1174
|
let mtimeMs = 0;
|
|
1023
1175
|
try {
|
|
1024
1176
|
mtimeMs = fs.statSync(file).mtimeMs;
|
|
1025
|
-
} catch {
|
|
1026
|
-
// The file may have been claimed or removed concurrently.
|
|
1027
|
-
}
|
|
1177
|
+
} catch {}
|
|
1028
1178
|
return { file, name, mtimeMs };
|
|
1029
1179
|
})
|
|
1030
1180
|
.filter((entry) => entry.mtimeMs > 0);
|
|
@@ -1038,9 +1188,7 @@ function pruneOutbox(env = process.env, nowMs = Date.now()) {
|
|
|
1038
1188
|
if (nowMs - entry.mtimeMs <= outboxMaxAgeMs) continue;
|
|
1039
1189
|
try {
|
|
1040
1190
|
fs.unlinkSync(entry.file);
|
|
1041
|
-
} catch {
|
|
1042
|
-
// A concurrent flush may have claimed it first.
|
|
1043
|
-
}
|
|
1191
|
+
} catch {}
|
|
1044
1192
|
}
|
|
1045
1193
|
entries = outboxRecordEntries(dir);
|
|
1046
1194
|
const overflow = entries.length - outboxMaxRecords;
|
|
@@ -1051,13 +1199,9 @@ function pruneOutbox(env = process.env, nowMs = Date.now()) {
|
|
|
1051
1199
|
.forEach((entry) => {
|
|
1052
1200
|
try {
|
|
1053
1201
|
fs.unlinkSync(entry.file);
|
|
1054
|
-
} catch {
|
|
1055
|
-
// Best-effort local retention cap.
|
|
1056
|
-
}
|
|
1202
|
+
} catch {}
|
|
1057
1203
|
});
|
|
1058
|
-
} catch {
|
|
1059
|
-
// Pruning is best-effort and must not affect the customer flow.
|
|
1060
|
-
}
|
|
1204
|
+
} catch {}
|
|
1061
1205
|
}
|
|
1062
1206
|
|
|
1063
1207
|
function recoverStaleOutboxClaims(env = process.env, nowMs = Date.now()) {
|
|
@@ -1073,13 +1217,9 @@ function recoverStaleOutboxClaims(env = process.env, nowMs = Date.now()) {
|
|
|
1073
1217
|
try {
|
|
1074
1218
|
if (fs.existsSync(originalPath)) fs.unlinkSync(claimedPath);
|
|
1075
1219
|
else fs.renameSync(claimedPath, originalPath);
|
|
1076
|
-
} catch {
|
|
1077
|
-
// Leave the claim for a later recovery attempt.
|
|
1078
|
-
}
|
|
1220
|
+
} catch {}
|
|
1079
1221
|
}
|
|
1080
|
-
} catch {
|
|
1081
|
-
// Recovery is best-effort.
|
|
1082
|
-
}
|
|
1222
|
+
} catch {}
|
|
1083
1223
|
}
|
|
1084
1224
|
|
|
1085
1225
|
function claimOutboxFile(file) {
|
|
@@ -1103,6 +1243,17 @@ function releaseOutboxClaim(claimedPath) {
|
|
|
1103
1243
|
}
|
|
1104
1244
|
}
|
|
1105
1245
|
|
|
1246
|
+
function outboxTargetFilePending(file) {
|
|
1247
|
+
try {
|
|
1248
|
+
if (fs.existsSync(file)) return true;
|
|
1249
|
+
const dir = path.dirname(file);
|
|
1250
|
+
const prefix = `${path.basename(file)}.sending.`;
|
|
1251
|
+
return fs.readdirSync(dir).some((name) => name.startsWith(prefix));
|
|
1252
|
+
} catch {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1106
1257
|
function readOutboxRecord(file) {
|
|
1107
1258
|
try {
|
|
1108
1259
|
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -1116,26 +1267,7 @@ function readOutboxRecord(file) {
|
|
|
1116
1267
|
}
|
|
1117
1268
|
|
|
1118
1269
|
function writeUploadStatus(status, env = process.env) {
|
|
1119
|
-
|
|
1120
|
-
const statusPath = uploadStatusPath(env);
|
|
1121
|
-
ensurePrivateDir(path.dirname(statusPath));
|
|
1122
|
-
const next = {
|
|
1123
|
-
schema_ver: 1,
|
|
1124
|
-
updated_at: Date.now(),
|
|
1125
|
-
...status
|
|
1126
|
-
};
|
|
1127
|
-
const tempPath = path.join(path.dirname(statusPath), `.${path.basename(statusPath)}.${process.pid}.tmp`);
|
|
1128
|
-
fs.writeFileSync(tempPath, `${JSON.stringify(next)}\n`, { mode: 0o600 });
|
|
1129
|
-
try {
|
|
1130
|
-
fs.chmodSync(tempPath, 0o600);
|
|
1131
|
-
} catch {
|
|
1132
|
-
// chmod is best-effort on Windows.
|
|
1133
|
-
}
|
|
1134
|
-
fs.renameSync(tempPath, statusPath);
|
|
1135
|
-
return true;
|
|
1136
|
-
} catch {
|
|
1137
|
-
return false;
|
|
1138
|
-
}
|
|
1270
|
+
return writePrivateJson(uploadStatusPath(env), { schema_ver: 1, updated_at: Date.now(), ...status });
|
|
1139
1271
|
}
|
|
1140
1272
|
|
|
1141
1273
|
function readUploadStatus(env = process.env) {
|
|
@@ -1161,21 +1293,7 @@ function readPermissionedFlushState(env = process.env, nowMs = Date.now()) {
|
|
|
1161
1293
|
}
|
|
1162
1294
|
|
|
1163
1295
|
function writePermissionedFlushState(state, env = process.env) {
|
|
1164
|
-
|
|
1165
|
-
const statePath = permissionedFlushStatePath(env);
|
|
1166
|
-
ensurePrivateDir(path.dirname(statePath));
|
|
1167
|
-
const tempPath = path.join(path.dirname(statePath), `.${path.basename(statePath)}.${process.pid}.tmp`);
|
|
1168
|
-
fs.writeFileSync(tempPath, `${JSON.stringify({ schema_ver: 1, ...state })}\n`, { mode: 0o600 });
|
|
1169
|
-
try {
|
|
1170
|
-
fs.chmodSync(tempPath, 0o600);
|
|
1171
|
-
} catch {
|
|
1172
|
-
// chmod is best-effort on Windows.
|
|
1173
|
-
}
|
|
1174
|
-
fs.renameSync(tempPath, statePath);
|
|
1175
|
-
return true;
|
|
1176
|
-
} catch {
|
|
1177
|
-
return false;
|
|
1178
|
-
}
|
|
1296
|
+
return writePrivateJson(permissionedFlushStatePath(env), { schema_ver: 1, ...state });
|
|
1179
1297
|
}
|
|
1180
1298
|
|
|
1181
1299
|
function networkUploadFailure(status) {
|
|
@@ -1227,12 +1345,13 @@ export function permissionedFlushDecision(env = process.env, nowMs = Date.now(),
|
|
|
1227
1345
|
|
|
1228
1346
|
export async function flushOutbox(env = process.env, {
|
|
1229
1347
|
limit = outboxFlushLimit,
|
|
1348
|
+
targetFile = '',
|
|
1230
1349
|
timeoutMs = uploadTimeoutMs,
|
|
1231
1350
|
sender = sendSyncLogReportDetailed
|
|
1232
1351
|
} = {}) {
|
|
1233
1352
|
const result = { attempted: 0, sent: 0, retained: 0, invalid: 0 };
|
|
1234
1353
|
if (!telemetryUploadEnabled(env)) return result;
|
|
1235
|
-
const files = outboxFiles(env
|
|
1354
|
+
const files = outboxFiles(env, { targetFile, limit });
|
|
1236
1355
|
for (const file of files) {
|
|
1237
1356
|
const claimedFile = claimOutboxFile(file);
|
|
1238
1357
|
if (!claimedFile) continue;
|
|
@@ -1241,9 +1360,7 @@ export async function flushOutbox(env = process.env, {
|
|
|
1241
1360
|
result.invalid += 1;
|
|
1242
1361
|
try {
|
|
1243
1362
|
fs.unlinkSync(claimedFile);
|
|
1244
|
-
} catch {
|
|
1245
|
-
// best-effort cleanup.
|
|
1246
|
-
}
|
|
1363
|
+
} catch {}
|
|
1247
1364
|
continue;
|
|
1248
1365
|
}
|
|
1249
1366
|
const report = record.report?.item && record.report?.options ? record.report : buildSyncLogReport(record.event, env);
|
|
@@ -1251,9 +1368,7 @@ export async function flushOutbox(env = process.env, {
|
|
|
1251
1368
|
result.invalid += 1;
|
|
1252
1369
|
try {
|
|
1253
1370
|
fs.unlinkSync(claimedFile);
|
|
1254
|
-
} catch {
|
|
1255
|
-
// best-effort cleanup.
|
|
1256
|
-
}
|
|
1371
|
+
} catch {}
|
|
1257
1372
|
continue;
|
|
1258
1373
|
}
|
|
1259
1374
|
result.attempted += 1;
|
|
@@ -1262,18 +1377,13 @@ export async function flushOutbox(env = process.env, {
|
|
|
1262
1377
|
const rawSendResult = await sender(report, { timeoutMs });
|
|
1263
1378
|
sendResult = typeof rawSendResult === 'boolean' ? { ok: rawSendResult } : rawSendResult;
|
|
1264
1379
|
} catch (error) {
|
|
1265
|
-
sendResult =
|
|
1266
|
-
ok: false,
|
|
1267
|
-
error_code: safeDimension(error?.code || error?.name || 'SENDER_THROW', 'SENDER_THROW', 80),
|
|
1268
|
-
error_message: safeDimension(error?.message || 'sender threw', 'sender threw', 240)
|
|
1269
|
-
};
|
|
1380
|
+
sendResult = failedResult(error, 'SENDER_THROW', 'sender threw');
|
|
1270
1381
|
}
|
|
1271
1382
|
if (sendResult?.ok) {
|
|
1272
1383
|
result.sent += 1;
|
|
1273
1384
|
try {
|
|
1274
1385
|
fs.unlinkSync(claimedFile);
|
|
1275
1386
|
} catch {
|
|
1276
|
-
// If delete fails, keep the record for a later retry.
|
|
1277
1387
|
result.retained += 1;
|
|
1278
1388
|
releaseOutboxClaim(claimedFile);
|
|
1279
1389
|
}
|
|
@@ -1303,22 +1413,24 @@ export async function flushOutbox(env = process.env, {
|
|
|
1303
1413
|
return result;
|
|
1304
1414
|
}
|
|
1305
1415
|
|
|
1306
|
-
function spawnOutboxFlush(env = process.env) {
|
|
1416
|
+
function spawnOutboxFlush(env = process.env, { delayMs = 0 } = {}) {
|
|
1307
1417
|
if (!telemetryUploadEnabled(env)) return false;
|
|
1308
1418
|
try {
|
|
1419
|
+
const childEnv = {
|
|
1420
|
+
...env,
|
|
1421
|
+
ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || String(uploadTimeoutMs)
|
|
1422
|
+
};
|
|
1423
|
+
if (delayMs > 0) childEnv.ALIPAY_AIPAY_TELEMETRY_FLUSH_DELAY_MS = String(Math.min(delayMs, 30000));
|
|
1309
1424
|
const child = spawn(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
|
|
1310
1425
|
detached: true,
|
|
1311
1426
|
stdio: 'ignore',
|
|
1312
|
-
env:
|
|
1313
|
-
...env,
|
|
1314
|
-
ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || String(uploadTimeoutMs)
|
|
1315
|
-
}
|
|
1427
|
+
env: childEnv
|
|
1316
1428
|
});
|
|
1317
1429
|
child.on('error', (error) => {
|
|
1318
1430
|
writeUploadStatus({
|
|
1319
1431
|
status: 'spawn_failed',
|
|
1320
1432
|
error_code: safeDimension(error?.code || error?.name || 'UPLOAD_SPAWN_FAILED', 'UPLOAD_SPAWN_FAILED', 80),
|
|
1321
|
-
error_message: safeDimension(error?.message || '
|
|
1433
|
+
error_message: safeDimension(error?.message || 'upload spawn failed', 'upload spawn failed', 240)
|
|
1322
1434
|
}, env);
|
|
1323
1435
|
});
|
|
1324
1436
|
child.unref();
|
|
@@ -1327,18 +1439,59 @@ function spawnOutboxFlush(env = process.env) {
|
|
|
1327
1439
|
writeUploadStatus({
|
|
1328
1440
|
status: 'spawn_failed',
|
|
1329
1441
|
error_code: 'UPLOAD_SPAWN_FAILED',
|
|
1330
|
-
error_message: '
|
|
1442
|
+
error_message: 'upload spawn failed'
|
|
1331
1443
|
}, env);
|
|
1332
1444
|
return false;
|
|
1333
1445
|
}
|
|
1334
1446
|
}
|
|
1335
1447
|
|
|
1448
|
+
function foregroundFlushTerminalOutboxFile(file, env = process.env) {
|
|
1449
|
+
if (!telemetryUploadEnabled(env) || !file) return false;
|
|
1450
|
+
const parsed = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_TERMINAL_FLUSH_TIMEOUT_MS);
|
|
1451
|
+
const timeoutMs = parsed && parsed > 0 ? Math.min(parsed, 30000) : terminalForegroundUploadTimeoutMs;
|
|
1452
|
+
const parsedAttempts = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_TERMINAL_FLUSH_ATTEMPTS);
|
|
1453
|
+
const attempts = parsedAttempts && parsedAttempts > 0
|
|
1454
|
+
? Math.min(parsedAttempts, 3)
|
|
1455
|
+
: terminalForegroundUploadMaxAttempts;
|
|
1456
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
1457
|
+
try {
|
|
1458
|
+
execFileSync(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
|
|
1459
|
+
stdio: 'ignore',
|
|
1460
|
+
timeout: timeoutMs + 800,
|
|
1461
|
+
env: {
|
|
1462
|
+
...env,
|
|
1463
|
+
ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: String(timeoutMs),
|
|
1464
|
+
ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE: file
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
} catch (error) {
|
|
1468
|
+
writeUploadStatus({
|
|
1469
|
+
status: 'failed',
|
|
1470
|
+
error_code: safeDimension(error?.code || error?.signal || error?.name || 'TERMINAL_FLUSH_FAILED', 'TERMINAL_FLUSH_FAILED', 80),
|
|
1471
|
+
error_message: 'terminal flush failed'
|
|
1472
|
+
}, env);
|
|
1473
|
+
}
|
|
1474
|
+
if (!outboxTargetFilePending(file)) return true;
|
|
1475
|
+
const retryable = Boolean(networkUploadFailure(readUploadStatus(env)));
|
|
1476
|
+
if (!retryable || attempt >= attempts) return false;
|
|
1477
|
+
sleepSync(terminalForegroundUploadRetryDelayMs);
|
|
1478
|
+
}
|
|
1479
|
+
return !outboxTargetFilePending(file);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1336
1482
|
function scheduleSyncLogUpload(event, env = process.env) {
|
|
1337
1483
|
if (!telemetryUploadEnabled(env)) return false;
|
|
1338
|
-
|
|
1339
|
-
|
|
1484
|
+
const queuedFile = writeOutboxRecord(event, env);
|
|
1485
|
+
if (!queuedFile) {
|
|
1486
|
+
recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'outbox write failed', env);
|
|
1340
1487
|
return false;
|
|
1341
1488
|
}
|
|
1489
|
+
if (event?.stage === 'run.terminal') {
|
|
1490
|
+
if (!foregroundFlushTerminalOutboxFile(queuedFile, env)) {
|
|
1491
|
+
spawnOutboxFlush(env, { delayMs: terminalBackgroundRetryDelayMs });
|
|
1492
|
+
}
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1342
1495
|
return spawnOutboxFlush(env);
|
|
1343
1496
|
}
|
|
1344
1497
|
|
|
@@ -1347,7 +1500,6 @@ function emitTelemetryError(error) {
|
|
|
1347
1500
|
const message = safeDimension(error instanceof Error ? error.message : String(error), 'unknown', 240);
|
|
1348
1501
|
process.stderr.write(`${internalPrefix}TELEMETRY_ERROR=${message}\n`);
|
|
1349
1502
|
} catch {
|
|
1350
|
-
// Telemetry must stay nonblocking.
|
|
1351
1503
|
}
|
|
1352
1504
|
}
|
|
1353
1505
|
|
|
@@ -1380,12 +1532,41 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1380
1532
|
}
|
|
1381
1533
|
return 0;
|
|
1382
1534
|
}
|
|
1535
|
+
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());
|
|
1538
|
+
if (counts) {
|
|
1539
|
+
process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE=${JSON.stringify(counts)}\n`);
|
|
1540
|
+
} else {
|
|
1541
|
+
process.stderr.write(`${internalPrefix}USER_INPUT_COUNT_STATE_MISSING=true\n`);
|
|
1542
|
+
}
|
|
1543
|
+
return 0;
|
|
1544
|
+
}
|
|
1545
|
+
if (command === 'mark-flow-start' && args._.length === 0) {
|
|
1546
|
+
assertCliArgs(args, new Set(), 0);
|
|
1547
|
+
const counts = markTelemetryFlowStart(process.env, Date.now());
|
|
1548
|
+
if (counts) {
|
|
1549
|
+
process.stderr.write(`${internalPrefix}USER_INPUT_FLOW_START_STATE=${JSON.stringify(counts)}\n`);
|
|
1550
|
+
} else {
|
|
1551
|
+
process.stderr.write(`${internalPrefix}USER_INPUT_FLOW_START_STATE_MISSING=true\n`);
|
|
1552
|
+
}
|
|
1553
|
+
return 0;
|
|
1554
|
+
}
|
|
1383
1555
|
if (command === 'upload-status' && args._.length === 0) {
|
|
1384
1556
|
assertCliArgs(args, new Set(), 0);
|
|
1385
1557
|
const status = readUploadStatus(process.env) || { schema_ver: 1, status: 'unknown' };
|
|
1386
1558
|
process.stderr.write(`${internalPrefix}TELEMETRY_UPLOAD_STATUS=${JSON.stringify(status)}\n`);
|
|
1387
1559
|
return 0;
|
|
1388
1560
|
}
|
|
1561
|
+
if (command === 'permissioned-flush-status' && args._.length === 0) {
|
|
1562
|
+
assertCliArgs(args, new Set(), 0);
|
|
1563
|
+
const decision = permissionedFlushDecision(process.env, Date.now(), { markRequested: false });
|
|
1564
|
+
process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_STATUS=${JSON.stringify(decision)}\n`);
|
|
1565
|
+
if (decision.needed) {
|
|
1566
|
+
process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_CANDIDATE=true\n`);
|
|
1567
|
+
}
|
|
1568
|
+
return 0;
|
|
1569
|
+
}
|
|
1389
1570
|
if (command === 'permissioned-flush-needed' && args._.length === 0) {
|
|
1390
1571
|
assertCliArgs(args, new Set(), 0);
|
|
1391
1572
|
const decision = permissionedFlushDecision(process.env, Date.now(), { markRequested: true });
|
|
@@ -1405,7 +1586,12 @@ export async function runTelemetryCli(argv = process.argv) {
|
|
|
1405
1586
|
assertCliArgs(args, new Set(), 0);
|
|
1406
1587
|
if (!telemetryEnabled(process.env)) return 0;
|
|
1407
1588
|
const timeout = Number(process.env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || uploadTimeoutMs);
|
|
1408
|
-
|
|
1589
|
+
const delay = parseNonNegativeInteger(process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_DELAY_MS);
|
|
1590
|
+
if (delay && delay > 0) sleepSync(Math.min(delay, 30000));
|
|
1591
|
+
await flushOutbox(process.env, {
|
|
1592
|
+
targetFile: process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE || '',
|
|
1593
|
+
timeoutMs: Number.isFinite(timeout) ? timeout : uploadTimeoutMs
|
|
1594
|
+
});
|
|
1409
1595
|
return 0;
|
|
1410
1596
|
}
|
|
1411
1597
|
if (!telemetryEnabled(process.env)) return 0;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
- stage/
|
|
2
|
-
-
|
|
3
|
-
- run
|
|
4
|
-
-
|
|
5
|
-
- 补传:`telemetry upload-status` 只读。最近失败为网络错误(含 `ENOTFOUND|EAI_AGAIN|ETIMEDOUT`)且 outbox 积压时,执行 `telemetry permissioned-flush-needed`;仅 `PERMISSIONED_FLUSH_NEEDED=true` 时可申请最小权限执行 `telemetry flush-outbox`:HTTPS POST `https://collect.alipay.com/yuyan/`(`collect.alipay.com:443`,路径 `/yuyan/`)。说明:`是否允许 Agent 上报 Skill 使用过程的统计信息?只含产品类别、时间、Skill 版本、匿名设备标识、人机交互次数等用于帮助改善用户体验的非敏感信息,不会采集包含源代码、用户输入内容等任何敏感信息。` 同一 Agent 会话只请求一次,状态键抑制 12h。`flush-outbox` 后台补传;拒绝、失败或非 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
|
+
- 计数:每条用户输入到达当前 run 后立刻执行 `telemetry count-user-input --phase before-flow|after-flow` 写本地 run state,不联网;触发 run 的首条消息计 before-flow。`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` 自动标记;用户 kill 未终态不补报。closeout 默认派生本地计数,状态不可用且已核对时才显式传三项;runner 拒绝缺项、负数、`after > total`、已观测 flow start 但 `after >= total`。终态只追加计数,不新增确认点、stage 或独立上报。`run.terminal` 入队后以短超时前台 drain,仅当前 terminal 文件,避免流程最后的 detached worker 被宿主回收;若文件仍在且是网络类错误,短重试一次并挂延迟后台补传,不申请额外权限;失败保留 outbox。`c4=ext_info`,计数合法且有 `root_intent` 才写 JSON。
|
|
4
|
+
- 补传:不为权限探测新增网络事件;实际低敏上传就是自然探测,宿主已允许访问 collector 时不额外申请权限。`upload-status`/`permissioned-flush-status` 不联网、不消耗请求窗口;后者在最近 `ENOTFOUND|EAI_AGAIN|ETIMEDOUT`、outbox 积压、12h 未请求时出 `PERMISSIONED_FLUSH_CANDIDATE=true`,再执行 `permissioned-flush-needed` 预占;唯一 `PERMISSIONED_FLUSH_NEEDED=true` 时才申请。`flush-outbox` 后台补传:POST 到 `collect.alipay.com:443`,路径 `/yuyan/`。说明:`是否允许 Agent 上报 Skill 使用过程的统计信息?只含产品类别、时间、Skill 版本、匿名设备标识、人机交互次数等用于帮助改善用户体验的非敏感信息,不会采集包含源代码、用户输入内容等任何敏感信息。` 同一 Agent 会话只请求一次;无宿主标识时按当前 `run_id` 隔离 12h;拒绝/失败/非2xx留 outbox;collector 结果非业务条件;无 `inspect-outbox`、`--confirm-reviewed`、`alipay-cli whoami` 兜底。
|
|
@@ -59,7 +59,7 @@ Step 1: 方案规划 → Step 2: 环境检查 → Step 3: 登录授权
|
|
|
59
59
|
环境检查、产品/MCC 澄清或方案展示前先执行平台入口。`onboarding_only` 正常由 `onboarding-bootstrap --root-intent onboarding_only` 包含本动作和 entry 加载;仅原子 fallback 或 `full_process` 衔接时执行:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-message platform-entry
|
|
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>
|
|
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
|
|
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
|
|
|
@@ -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"]]
|
|
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`。合并展示不得改写完成条件,待审核、待设置公钥或待人工配置仍不得表述为应用发布完成。
|
|
@@ -41,14 +41,14 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
|
|
|
41
41
|
| 意图 | 判定 | 入口 | 后续按需加载 |
|
|
42
42
|
|---|---|---|---|
|
|
43
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
|
|
44
|
+
| `onboarding_only` | 只要求签约、入驻或产品开通,未同时要求代码开发 | 执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" onboarding-bootstrap --root-intent onboarding_only` | 当前阶段的授权、查询或写分支模块 |
|
|
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
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 异步记录。
|
|
50
50
|
|
|
51
|
-
遥测默认开启,`ALIPAY_AIPAY_TELEMETRY=0|false|off|no`
|
|
51
|
+
遥测默认开启,`ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 关闭事件/outbox/上传/flush;私有 run context 仍供 `PLATFORM_ID` 与终态生命周期。失败静默,不改变业务/消息/确认,stdout 空且 stderr 内部行不对客。`integration_only` 由 checklist 终结;Onboarding 由 closeout 或登记提前终止消息终结。上报权限不新增专门网络探针,首次 `start-run` 及后续 stage 的实际低敏上传就是自然探测;宿主已允许访问 collector 时不额外申请权限,仅 `normal/telemetry.md` 的不联网状态命令确认网络失败和 outbox 积压后,才允许一次补传权限申请。Agent 按 `normal/telemetry.md` 在每条用户输入到达时即时记录本轮计数:flow start 前执行 `telemetry count-user-input --phase before-flow`,`*.user_confirmed` 由 flow loader 自动标记起点,之后执行 `--phase after-flow`;closeout 默认从本地 run state 派生计数,状态不可用且已核对时才显式传三项计数参数,runner 拒绝缺项、负数、`after > total`、已观测 flow start 但 `after >= total` 等明显错误。不新增业务确认点或独立上报;入队后当前 `run.terminal` 文件做短超时前台 drain,失败仍保留 outbox 且不影响业务收口,网络类失败按维护文档短重试/后台兜底。after-flow 以方案后 `*.user_confirmed` 为起点;前置澄清和候选不计。脚本以当前 `run_id` 为 `PLATFORM_ID`,Agent 不手写环境前缀或改 MCP payload;维护诊断、outbox、上传和 flush 细节只在 `normal/telemetry.md` 按需读取。
|
|
52
52
|
|
|
53
53
|
三种入口均只在无法从已有产品名称或业务描述可靠确定唯一产品时执行 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
54
|
|