@alipay/alipay-aipay 1.2.2 → 1.4.9

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.
Files changed (98) hide show
  1. package/README.md +5 -0
  2. package/dist/commands/install.d.ts.map +1 -1
  3. package/dist/commands/install.js +13 -12
  4. package/dist/commands/install.js.map +1 -1
  5. package/dist/commands/uninstall.d.ts.map +1 -1
  6. package/dist/commands/uninstall.js +17 -9
  7. package/dist/commands/uninstall.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +5 -5
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/index.js +4 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/utils/config.d.ts +5 -3
  14. package/dist/utils/config.d.ts.map +1 -1
  15. package/dist/utils/config.js +34 -12
  16. package/dist/utils/config.js.map +1 -1
  17. package/dist/utils/file.d.ts +8 -0
  18. package/dist/utils/file.d.ts.map +1 -1
  19. package/dist/utils/file.js +161 -9
  20. package/dist/utils/file.js.map +1 -1
  21. package/dist/utils/jq.d.ts.map +1 -1
  22. package/dist/utils/jq.js +8 -0
  23. package/dist/utils/jq.js.map +1 -1
  24. package/package.json +6 -5
  25. package/skill/SKILL.md +15 -169
  26. package/skill/VERSION +1 -1
  27. package/skill/references/integration/flow.md +321 -292
  28. package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
  29. package/skill/references/integration/modules/checklist.md +41 -27
  30. package/skill/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +199 -0
  31. package/skill/references/integration/modules/code-examples/csharp/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +8 -4
  32. package/skill/references/integration/modules/code-examples/csharp/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +6 -2
  33. package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +3 -1
  34. package/skill/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +184 -0
  35. package/skill/references/integration/modules/code-examples/java/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +4 -1
  36. package/skill/references/integration/modules/code-examples/java/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +5 -2
  37. package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +3 -1
  38. package/skill/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +135 -0
  39. package/skill/references/integration/modules/code-examples/nodejs/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +16 -10
  40. package/skill/references/integration/modules/code-examples/nodejs/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +10 -4
  41. package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +2 -1
  42. package/skill/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +157 -0
  43. package/skill/references/integration/modules/code-examples/php/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +4 -1
  44. package/skill/references/integration/modules/code-examples/php/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +5 -2
  45. package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +2 -1
  46. package/skill/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +128 -0
  47. package/skill/references/integration/modules/code-examples/python/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +4 -1
  48. package/skill/references/integration/modules/code-examples/python/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +5 -1
  49. package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +4 -2
  50. package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +53 -0
  51. package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
  52. package/skill/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
  53. package/skill/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
  54. package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
  55. package/skill/references/integration/modules/interface-guide.md +37 -72
  56. package/skill/references/integration/modules/product-decision.md +10 -27
  57. package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +62 -94
  58. package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +43 -40
  59. package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +93 -114
  60. package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +357 -147
  61. package/skill/references/integration/modules/scripts/sandbox_config.sh +299 -0
  62. package/skill/references/integration/modules/troubleshooting-flow.md +28 -0
  63. package/skill/references/normal/alipay-cli-env.md +15 -5
  64. package/skill/references/normal/full-process-routing.md +80 -0
  65. package/skill/references/normal/json/customer-messages.json +937 -0
  66. package/skill/references/normal/json/policy-rules.json +179 -0
  67. package/skill/references/normal/rejection-guide.md +11 -6
  68. package/skill/references/normal/scripts/alipay_cli_env.mjs +29 -0
  69. package/skill/references/normal/scripts/alipay_cli_env.sh +21 -0
  70. package/skill/references/normal/scripts/common.sh +87 -0
  71. package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
  72. package/skill/references/normal/scripts/integration_message_runner.mjs +801 -0
  73. package/skill/references/normal/scripts/onboarding_message_runner.mjs +436 -0
  74. package/skill/references/normal/scripts/open_official_url.sh +82 -0
  75. package/skill/references/normal/scripts/platform_compat.mjs +39 -0
  76. package/skill/references/normal/scripts/project_route_inspector.sh +132 -0
  77. package/skill/references/normal/scripts/python_script_runner.mjs +56 -0
  78. package/skill/references/normal/scripts/render_customer_message.mjs +1263 -0
  79. package/skill/references/normal/scripts/self_update.mjs +160 -0
  80. package/skill/references/normal/scripts/telemetry.mjs +1278 -0
  81. package/skill/references/normal/self-update.md +18 -51
  82. package/skill/references/onboarding/flow.md +381 -283
  83. package/skill/references/onboarding/modules/app-release.md +132 -179
  84. package/skill/references/onboarding/modules/authorization.md +53 -64
  85. package/skill/references/onboarding/modules/error-handling.md +31 -34
  86. package/skill/references/onboarding/modules/product-sign.md +62 -66
  87. package/skill/references/onboarding/modules/scripts/app.sh +397 -158
  88. package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +38 -22
  89. package/skill/references/onboarding/modules/scripts/auth.sh +479 -167
  90. package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
  91. package/skill/references/onboarding/modules/scripts/error_handler.sh +100 -130
  92. package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
  93. package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -70
  94. package/skill/references/onboarding/modules/scripts/service.sh +153 -89
  95. package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
  96. package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +16 -14
  97. package/skill/references/onboarding/modules/service-registration.md +67 -72
  98. package/skill/references/runtime-entry.md +62 -0
@@ -0,0 +1,1278 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import https from 'node:https';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import process from 'node:process';
7
+ import { execFileSync, spawn } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const internalPrefix = 'ALIPAY_AIPAY_INTERNAL:';
11
+ const disableValues = new Set(['0', 'false', 'off', 'no']);
12
+ const canonicalProductTypes = new Set(['AIPAY', 'WEBPAY', 'APPPAY', 'UNKNOWN']);
13
+ const allowedRootIntents = new Set(['integration_only', 'onboarding_only', 'full_process']);
14
+ const syncLogBizType = 'yuyanmonitorl';
15
+ const syncLogServerURL = 'https://collect.alipay.com/yuyan/';
16
+ const syncLogMonitorVersion = 'alipay-aipay-skill:sync-log';
17
+ const syncLogCode = '1005';
18
+ const syncLogMsg = 'alipay_aipay_skill_event';
19
+ const syncLogYuyanID = '180020010001290755';
20
+ const uploadTimeoutMs = 2500;
21
+ const outboxFlushLimit = 50;
22
+ const outboxClaimTtlMs = 5 * 60 * 1000;
23
+ const outboxMaxRecords = 1000;
24
+ const outboxMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
25
+ const permissionedFlushTtlMs = 12 * 60 * 60 * 1000;
26
+ const platformProjectIdEnvKeys = Object.freeze(['COZE_PROJECT_ID', 'MEOO_PROJECT_ID']);
27
+ const permissionedFlushNetworkErrors = new Set([
28
+ 'ENOTFOUND',
29
+ 'EAI_AGAIN',
30
+ 'ECONNREFUSED',
31
+ 'ECONNRESET',
32
+ 'ETIMEDOUT',
33
+ 'EHOSTUNREACH',
34
+ 'ENETUNREACH',
35
+ 'NETWORK_UNREACHABLE'
36
+ ]);
37
+ const productByVariant = { AIPAY: 'AIPAY', WEBPAY: 'WEBPAY', APPPAY: 'APPPAY' };
38
+ const productByName = { '按量付费': 'AIPAY', '网站支付': 'WEBPAY', 'APP 支付': 'APPPAY' };
39
+ const productAliases = {
40
+ aipay: 'AIPAY',
41
+ webpay: 'WEBPAY',
42
+ apppay: 'APPPAY',
43
+ unknown: 'UNKNOWN',
44
+ AIPAY: 'AIPAY',
45
+ WEBPAY: 'WEBPAY',
46
+ APPPAY: 'APPPAY',
47
+ UNKNOWN: 'UNKNOWN'
48
+ };
49
+
50
+ export const approvedStages = Object.freeze([
51
+ 'run.intent_detected',
52
+ 'run.product_clarify.start',
53
+ 'integration.context_prepare.start',
54
+ 'integration.env_prepare.start',
55
+ 'integration.start_confirm.start',
56
+ 'integration.user_confirmed',
57
+ 'integration.pre_dev_reading.start',
58
+ 'integration.code_dev.start',
59
+ 'integration.local_verify.start',
60
+ 'integration.sandbox_acceptance.start',
61
+ 'integration.checklist.start',
62
+ 'integration.closeout.start',
63
+ 'onboarding.mcc_prepare.start',
64
+ 'onboarding.plan_confirm.start',
65
+ 'onboarding.user_confirmed',
66
+ 'onboarding.auth.start',
67
+ 'onboarding.discovery.start',
68
+ 'onboarding.branch_summary.start',
69
+ 'onboarding.material_collect.start',
70
+ 'onboarding.signing.start',
71
+ 'onboarding.service.start',
72
+ 'onboarding.application.start',
73
+ 'onboarding.closeout.start',
74
+ 'run.terminal'
75
+ ]);
76
+
77
+ const stageRules = new Map(approvedStages.map((stage) => [stage, {
78
+ stage,
79
+ flow: stage.startsWith('integration.')
80
+ ? 'integration'
81
+ : stage.startsWith('onboarding.')
82
+ ? 'onboarding'
83
+ : 'common'
84
+ }]));
85
+
86
+ const messageStageRules = {
87
+ 'product.unsupported': 'run.terminal',
88
+ 'product.clarify': 'run.product_clarify.start',
89
+
90
+ 'integration.project_path.required': 'integration.context_prepare.start',
91
+ 'integration.context.required': 'integration.context_prepare.start',
92
+ 'integration.project_candidates.select': 'integration.context_prepare.start',
93
+ 'integration.start.confirm': 'integration.start_confirm.start',
94
+ 'integration.checklist.result': 'integration.closeout.start',
95
+
96
+ 'onboarding.enterprise.unsupported': 'run.terminal',
97
+ 'onboarding.plan.confirm': 'onboarding.plan_confirm.start',
98
+ 'onboarding.discovery.summary': 'onboarding.branch_summary.start',
99
+ 'materials.category.collect': 'onboarding.material_collect.start',
100
+ 'process.partial_result': 'onboarding.closeout.start'
101
+ };
102
+
103
+ function safeText(value) {
104
+ return typeof value === 'string' && /^[A-Za-z0-9_.:-]+$/.test(value) ? value : '';
105
+ }
106
+
107
+ function safeDimension(value, fallback = 'unknown', maxLength = 80) {
108
+ if (typeof value !== 'string') return fallback;
109
+ const trimmed = value.trim().replace(/[,\^=\r\n]/g, '_');
110
+ if (!trimmed) return fallback;
111
+ return trimmed.slice(0, maxLength);
112
+ }
113
+
114
+ function normalizeSwitch(value) {
115
+ return String(value || '').trim().toLowerCase();
116
+ }
117
+
118
+ export function telemetryEnabled(env = process.env) {
119
+ return !disableValues.has(normalizeSwitch(env.ALIPAY_AIPAY_TELEMETRY));
120
+ }
121
+
122
+ export function telemetryUploadEnabled(env = process.env) {
123
+ return telemetryEnabled(env)
124
+ && !disableValues.has(normalizeSwitch(env.ALIPAY_AIPAY_TELEMETRY_UPLOAD))
125
+ && !isMaintenanceProcess(env);
126
+ }
127
+
128
+ function scriptPath() {
129
+ return fileURLToPath(import.meta.url);
130
+ }
131
+
132
+ function scriptDir() {
133
+ return path.dirname(scriptPath());
134
+ }
135
+
136
+ function skillRoot() {
137
+ return path.resolve(scriptDir(), '../../..');
138
+ }
139
+
140
+ function telemetryRoot() {
141
+ return path.join(os.tmpdir(), 'alipay-aipay-telemetry');
142
+ }
143
+
144
+ function isMaintenanceProcess(env = process.env, argv = process.argv) {
145
+ if (normalizeSwitch(env.ALIPAY_AIPAY_TELEMETRY_ALLOW_MAINTENANCE) === '1') return false;
146
+ if (env.NODE_TEST_CONTEXT) return true;
147
+ if (env.npm_lifecycle_event === 'test') return true;
148
+ return argv.some((arg) => /(^|[/\\])test[/\\].+\.test\.mjs$/u.test(String(arg || '')));
149
+ }
150
+
151
+ function stableStateKey(env = process.env) {
152
+ const source =
153
+ env.ALIPAY_AIPAY_SESSION_ID ||
154
+ platformProjectRunId(env) ||
155
+ env.CODEX_THREAD_ID ||
156
+ env.CODEX_SESSION_ID ||
157
+ env.CLAUDE_CODE_SESSION_ID ||
158
+ env.OPENCLAW_SESSION_ID ||
159
+ env.CURSOR_SESSION_ID ||
160
+ env.CURSOR_TRACE_ID ||
161
+ env.COZE_SESSION_ID ||
162
+ env.COZE_CONVERSATION_ID ||
163
+ env.HERMES_SESSION_ID ||
164
+ env.OPENAI_CONVERSATION_ID ||
165
+ env.SESSION_ID ||
166
+ env.CI_JOB_ID ||
167
+ '';
168
+ if (!source) return 'default';
169
+ if (safeText(source) && source.length <= 128) return source;
170
+ return `key-${crypto.createHash('sha256').update(source).digest('hex')}`;
171
+ }
172
+
173
+ function stateDir(env = process.env) {
174
+ if (env.ALIPAY_AIPAY_TELEMETRY_DIR) return env.ALIPAY_AIPAY_TELEMETRY_DIR;
175
+ return telemetryRoot();
176
+ }
177
+
178
+ function runStatePath(env = process.env) {
179
+ if (env.ALIPAY_AIPAY_RUN_STATE_FILE) return env.ALIPAY_AIPAY_RUN_STATE_FILE;
180
+ return path.join(stateDir(env), `${stableStateKey(env)}.json`);
181
+ }
182
+
183
+ function outboxDir(env = process.env) {
184
+ if (env.ALIPAY_AIPAY_TELEMETRY_OUTBOX_DIR) return env.ALIPAY_AIPAY_TELEMETRY_OUTBOX_DIR;
185
+ return path.join(stateDir(env), 'outbox');
186
+ }
187
+
188
+ function uploadStatusPath(env = process.env) {
189
+ if (env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_STATUS_FILE) return env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_STATUS_FILE;
190
+ return path.join(stateDir(env), 'upload-status.json');
191
+ }
192
+
193
+ function permissionedFlushStatePath(env = process.env) {
194
+ if (env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE) {
195
+ return env.ALIPAY_AIPAY_TELEMETRY_PERMISSIONED_FLUSH_STATE_FILE;
196
+ }
197
+ return path.join(stateDir(env), `${stableStateKey(env)}.permissioned-flush.json`);
198
+ }
199
+
200
+ export function telemetryStoragePaths(env = process.env) {
201
+ return {
202
+ state_dir: stateDir(env),
203
+ run_state_file: runStatePath(env),
204
+ outbox_dir: outboxDir(env),
205
+ upload_status_file: uploadStatusPath(env),
206
+ permissioned_flush_state_file: permissionedFlushStatePath(env)
207
+ };
208
+ }
209
+
210
+ function ensurePrivateDir(dir) {
211
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
212
+ try {
213
+ fs.chmodSync(dir, 0o700);
214
+ } catch {
215
+ // chmod is best-effort on Windows.
216
+ }
217
+ }
218
+
219
+ function readRunState(env = process.env) {
220
+ try {
221
+ const statePath = runStatePath(env);
222
+ const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
223
+ if (!parsed || typeof parsed !== 'object') return null;
224
+ if (parsed.schema_ver !== 1) return null;
225
+ if (!safeRunId(parsed.run_id)) return null;
226
+ const createdAt = Number(parsed.created_at);
227
+ const updatedAt = Number(parsed.updated_at);
228
+ if (!Number.isFinite(createdAt) || createdAt <= 0) return null;
229
+ if (!Number.isFinite(updatedAt) || updatedAt <= 0) return null;
230
+ return parsed;
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+
236
+ function canReadImplicitRunState(env = process.env) {
237
+ return !isMaintenanceProcess(env);
238
+ }
239
+
240
+ function recordLocalTelemetryFailure(errorCode, message, env = process.env) {
241
+ const safeCode = safeDimension(errorCode, 'LOCAL_TELEMETRY_FAILED', 80);
242
+ const safeMessage = safeDimension(message || safeCode, safeCode, 240);
243
+ writeUploadStatus({
244
+ status: 'local_failed',
245
+ error_code: safeCode,
246
+ error_message: safeMessage
247
+ }, env);
248
+ emitTelemetryError(new Error(safeCode));
249
+ }
250
+
251
+ function writeRunState(state, env = process.env) {
252
+ try {
253
+ const statePath = runStatePath(env);
254
+ ensurePrivateDir(path.dirname(statePath));
255
+ const tempPath = path.join(path.dirname(statePath), `.${path.basename(statePath)}.${process.pid}.tmp`);
256
+ fs.writeFileSync(tempPath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
257
+ try {
258
+ fs.chmodSync(tempPath, 0o600);
259
+ } catch {
260
+ // chmod is best-effort on Windows.
261
+ }
262
+ fs.renameSync(tempPath, statePath);
263
+ return true;
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
268
+
269
+ function outboxFilename(event) {
270
+ const timestamp = Number.isFinite(Number(event?.timestamp)) ? String(Number(event.timestamp)) : String(Date.now());
271
+ const eventID = safeText(event?.event_id) || crypto.randomUUID();
272
+ return `${timestamp}-${eventID}.json`;
273
+ }
274
+
275
+ export function enqueueMetricEvent(event, env = process.env) {
276
+ try {
277
+ if (!event) return false;
278
+ const dir = outboxDir(env);
279
+ ensurePrivateDir(dir);
280
+ const filename = outboxFilename(event);
281
+ const finalPath = path.join(dir, filename);
282
+ const tempPath = path.join(dir, `.${filename}.${process.pid}.tmp`);
283
+ const record = {
284
+ schema_ver: 1,
285
+ created_at: Date.now(),
286
+ event
287
+ };
288
+ fs.writeFileSync(tempPath, `${JSON.stringify(record)}\n`, { mode: 0o600 });
289
+ try {
290
+ fs.chmodSync(tempPath, 0o600);
291
+ } catch {
292
+ // chmod is best-effort on Windows.
293
+ }
294
+ fs.renameSync(tempPath, finalPath);
295
+ pruneOutbox(env);
296
+ return true;
297
+ } catch {
298
+ return false;
299
+ }
300
+ }
301
+
302
+ function safeRunId(value) {
303
+ if (typeof value !== 'string' || !value.trim()) return '';
304
+ // Sync-log uses comma-separated columns and ^/= delimiters without value escaping.
305
+ return /[\u0000-\u001f\u007f,^=]/u.test(value) ? '' : value;
306
+ }
307
+
308
+ function platformProjectRunId(env = process.env) {
309
+ for (const key of platformProjectIdEnvKeys) {
310
+ const value = safeRunId(env[key]);
311
+ if (value) return value;
312
+ }
313
+ if (env.MIAODA) {
314
+ return safeRunId(env.VITE_APP_ID) || safeRunId(env.PLATFORM_ID) || '';
315
+ }
316
+ return '';
317
+ }
318
+
319
+ function explicitRunIdFrom(context, env) {
320
+ return safeRunId(context.runId || context.run_id || env.ALIPAY_AIPAY_RUN_ID || env.ALIPAY_AIPAY_FLOW_RUN_ID);
321
+ }
322
+
323
+ function runIdFrom(context, env) {
324
+ const state = canReadImplicitRunState(env) ? readRunState(env) : null;
325
+ return safeRunId(platformProjectRunId(env) || explicitRunIdFrom(context, env) || state?.run_id);
326
+ }
327
+
328
+ function rootIntentFrom(context, env) {
329
+ const state = canReadImplicitRunState(env) ? readRunState(env) : null;
330
+ const value = context.rootIntent || context.root_intent || env.ALIPAY_AIPAY_ROOT_INTENT || state?.root_intent;
331
+ return allowedRootIntents.has(value) ? value : '';
332
+ }
333
+
334
+ function canonicalProductType(value) {
335
+ return typeof value === 'string' && productAliases[value] ? productAliases[value] : '';
336
+ }
337
+
338
+ function productTypeFrom({ ruleProductType, variantName, variables = {}, context = {}, env = process.env }) {
339
+ const explicit = context.productType || context.product_type || ruleProductType;
340
+ const explicitCanonical = canonicalProductType(explicit);
341
+ if (explicitCanonical) return explicitCanonical;
342
+ const variableCanonical = canonicalProductType(variables.productType);
343
+ if (variableCanonical) return variableCanonical;
344
+ if (productByVariant[variantName]) return productByVariant[variantName];
345
+ if (productByName[variables.productName]) return productByName[variables.productName];
346
+ if (productByName[context.productName]) return productByName[context.productName];
347
+ const stateCanonical = canReadImplicitRunState(env) ? canonicalProductType(readRunState(env)?.product_type) : '';
348
+ if (stateCanonical) return stateCanonical;
349
+ const envCanonical = canonicalProductType(env.ALIPAY_AIPAY_PRODUCT_TYPE);
350
+ if (envCanonical) return envCanonical;
351
+ return 'UNKNOWN';
352
+ }
353
+
354
+ export function stageMetricEvent(stage, context = {}, nowMs = Date.now(), env = process.env) {
355
+ const rule = stageRules.get(stage);
356
+ if (!rule) return null;
357
+ const state = canReadImplicitRunState(env) ? readRunState(env) : null;
358
+ const explicitRunId = platformProjectRunId(env) || explicitRunIdFrom(context, env);
359
+ if (state && (!explicitRunId || explicitRunId === state.run_id) && !runStateCompatibleWithEventContext(state, context)) {
360
+ return null;
361
+ }
362
+ const runId = runIdFrom(context, env);
363
+ if (!runId) return null;
364
+ const event = {
365
+ schema_ver: 2,
366
+ event_id: crypto.randomUUID(),
367
+ skill: 'alipay-aipay',
368
+ run_id: runId,
369
+ stage: rule.stage,
370
+ timestamp: nowMs,
371
+ flow: rule.flow,
372
+ product_type: productTypeFrom({ ruleProductType: context.productType || context.product_type, context, env })
373
+ };
374
+ if (!canonicalProductTypes.has(event.product_type)) event.product_type = 'UNKNOWN';
375
+ const rootIntent = rootIntentFrom(context, env);
376
+ if (rootIntent) event.root_intent = rootIntent;
377
+ return event;
378
+ }
379
+
380
+ export function messageMetricEvent(messageId, variantName, variables = {}, context = {}, nowMs = Date.now(), env = process.env) {
381
+ const stage = messageStageRules[messageId];
382
+ if (!stage) return null;
383
+ const stageEvent = stageMetricEvent(stage, {
384
+ ...context,
385
+ productType: productTypeFrom({ variantName, variables, context, env })
386
+ }, nowMs, env);
387
+ return stageEvent;
388
+ }
389
+
390
+ export function emitMetricEvent(event, { env = process.env, stream = process.stderr, upload = true } = {}) {
391
+ if (!telemetryEnabled(env) || !event) return false;
392
+ stream.write(`${internalPrefix}METRIC_EVENT=${JSON.stringify(event)}\n`);
393
+ if (upload) scheduleSyncLogUpload(event, env);
394
+ return true;
395
+ }
396
+
397
+ export function emitStageMetric(stage, context = {}) {
398
+ try {
399
+ return emitMetricEvent(stageMetricEvent(stage, context));
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+
405
+ export function emitMessageMetric(messageId, variantName, variables = {}, context = {}) {
406
+ try {
407
+ const event = messageMetricEvent(messageId, variantName, variables, context);
408
+ if (event?.stage === 'run.terminal' && telemetryEnabled()) updateExistingRunStateFromEvent(event);
409
+ return emitMetricEvent(event);
410
+ } catch {
411
+ return false;
412
+ }
413
+ }
414
+
415
+ export function emitTerminalMetric(context = {}) {
416
+ try {
417
+ if (!telemetryEnabled()) return false;
418
+ const event = stageMetricEvent('run.terminal', context);
419
+ if (!event) return false;
420
+ updateExistingRunStateFromEvent(event);
421
+ return emitMetricEvent(event);
422
+ } catch {
423
+ return false;
424
+ }
425
+ }
426
+
427
+ function parseCliArgs(argv) {
428
+ const command = argv[2];
429
+ const args = { _: [] };
430
+ for (let index = 3; index < argv.length; index += 1) {
431
+ const token = argv[index];
432
+ if (!token.startsWith('--')) {
433
+ args._.push(token);
434
+ continue;
435
+ }
436
+ const name = token.slice(2);
437
+ const value = argv[index + 1];
438
+ if (value === undefined || value.startsWith('--')) throw new Error(`参数 --${name} 缺少取值`);
439
+ args[name] = value;
440
+ index += 1;
441
+ }
442
+ return { command, args };
443
+ }
444
+
445
+ function contextFromCliArgs(args) {
446
+ return {
447
+ runId: args['run-id'],
448
+ rootIntent: args['root-intent'],
449
+ productType: args['product-type'],
450
+ productName: args['product-name']
451
+ };
452
+ }
453
+
454
+ function assertCliArgs(args, allowedOptions, positionalCount) {
455
+ for (const name of Object.keys(args)) {
456
+ if (name !== '_' && !allowedOptions.has(name)) throw new Error(`不支持参数 --${name}`);
457
+ }
458
+ if (args._.length !== positionalCount) throw new Error('invalid telemetry arguments');
459
+ }
460
+
461
+ function createRunState(args, env = process.env, nowMs = Date.now()) {
462
+ const runId = platformProjectRunId(env) || safeRunId(args['run-id']) || crypto.randomUUID();
463
+ const rootIntent = args['root-intent'];
464
+ if (!allowedRootIntents.has(rootIntent)) throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
465
+ const productType = canonicalProductType(args['product-type']) || 'UNKNOWN';
466
+ return {
467
+ schema_ver: 1,
468
+ run_id: runId,
469
+ root_intent: rootIntent,
470
+ product_type: productType,
471
+ created_at: nowMs,
472
+ updated_at: nowMs
473
+ };
474
+ }
475
+
476
+ function compatibleProductType(existingProductType, requestedProductType) {
477
+ const existing = canonicalProductType(existingProductType) || 'UNKNOWN';
478
+ const requested = canonicalProductType(requestedProductType) || 'UNKNOWN';
479
+ return existing === requested || existing === 'UNKNOWN' || requested === 'UNKNOWN';
480
+ }
481
+
482
+ function runStateCompatibleWithEventContext(state, context = {}) {
483
+ if (!state) return true;
484
+ const requestedRootIntent = context.rootIntent || context.root_intent;
485
+ if (requestedRootIntent && state.root_intent && state.root_intent !== requestedRootIntent) return false;
486
+ const requestedProductType = canonicalProductType(context.productType || context.product_type);
487
+ if (requestedProductType && !compatibleProductType(state.product_type, requestedProductType)) return false;
488
+ return true;
489
+ }
490
+
491
+ function mergeProductType(existingProductType, requestedProductType) {
492
+ const existing = canonicalProductType(existingProductType) || 'UNKNOWN';
493
+ const requested = canonicalProductType(requestedProductType) || 'UNKNOWN';
494
+ if (existing === 'UNKNOWN' && requested !== 'UNKNOWN') return requested;
495
+ if (requested === 'UNKNOWN' && existing !== 'UNKNOWN') return existing;
496
+ return requested;
497
+ }
498
+
499
+ function reusableRunState(existing, requestedRootIntent, requestedProductType) {
500
+ if (!existing || existing.terminal) return false;
501
+ if (existing.root_intent !== requestedRootIntent) return false;
502
+ return compatibleProductType(existing.product_type, requestedProductType);
503
+ }
504
+
505
+ function createOrReuseRunState(args, env = process.env, nowMs = Date.now()) {
506
+ const rootIntent = args['root-intent'];
507
+ if (!allowedRootIntents.has(rootIntent)) throw new Error('--root-intent 仅支持 integration_only、onboarding_only、full_process');
508
+ const requestedProductType = canonicalProductType(args['product-type']) || 'UNKNOWN';
509
+ const requestedRunId = platformProjectRunId(env) || safeRunId(args['run-id']);
510
+ const existing = readRunState(env);
511
+ if (reusableRunState(existing, rootIntent, requestedProductType) && (!requestedRunId || existing.run_id === requestedRunId)) {
512
+ return {
513
+ ...existing,
514
+ root_intent: rootIntent,
515
+ product_type: mergeProductType(existing.product_type, requestedProductType),
516
+ updated_at: nowMs
517
+ };
518
+ }
519
+ return createRunState(args, env, nowMs);
520
+ }
521
+
522
+ function updateRunStateFromEvent(event, env = process.env) {
523
+ const existing = readRunState(env) || {};
524
+ const next = {
525
+ ...existing,
526
+ schema_ver: 1,
527
+ run_id: event.run_id,
528
+ root_intent: event.root_intent || existing.root_intent || '',
529
+ product_type: event.product_type && event.product_type !== 'UNKNOWN' ? event.product_type : existing.product_type || 'UNKNOWN',
530
+ created_at: existing.created_at || Date.now(),
531
+ updated_at: Date.now()
532
+ };
533
+ if (event.stage === 'run.terminal') {
534
+ next.terminal = true;
535
+ next.terminal_at = Number.isFinite(Number(event.timestamp)) ? Number(event.timestamp) : Date.now();
536
+ } else if (existing.terminal && existing.run_id === event.run_id) {
537
+ next.terminal = existing.terminal;
538
+ next.terminal_at = existing.terminal_at;
539
+ }
540
+ writeRunState(next, env);
541
+ }
542
+
543
+ function updateExistingRunStateFromEvent(event, env = process.env) {
544
+ const existing = readRunState(env);
545
+ if (!existing) return false;
546
+ if (existing.run_id !== event.run_id) return false;
547
+ if (!runStateCompatibleWithEventContext(existing, {
548
+ rootIntent: event.root_intent,
549
+ productType: event.product_type
550
+ })) return false;
551
+ updateRunStateFromEvent(event, env);
552
+ return true;
553
+ }
554
+
555
+ function normalizeOS(platform = process.platform) {
556
+ switch (platform) {
557
+ case 'darwin':
558
+ return 'macOS';
559
+ case 'linux':
560
+ return 'Linux';
561
+ case 'win32':
562
+ return 'Windows';
563
+ default:
564
+ return 'Other';
565
+ }
566
+ }
567
+
568
+ function normalizePlatform(value) {
569
+ const normalized = String(value || '').trim().toLowerCase().replace(/[_\s-]/g, '');
570
+ if (normalized === 'codex') return 'codex';
571
+ if (normalized === 'claudecode' || normalized === 'claude') return 'claudecode';
572
+ if (normalized === 'cursor') return 'cursor';
573
+ if (normalized === 'coze') return 'coze';
574
+ if (normalized === 'meoo') return 'meoo';
575
+ if (normalized === 'miaoda') return 'miaoda';
576
+ if (normalized === 'openclaw') return 'openclaw';
577
+ if (normalized === 'traesolo') return 'traesolo';
578
+ if (normalized === 'qoder') return 'qoder';
579
+ if (/^[a-z0-9.]{1,40}$/.test(normalized) && normalized !== 'unknown') return normalized;
580
+ return 'unknown';
581
+ }
582
+
583
+ function platformFromEnv(env = process.env) {
584
+ if (env.ALIPAY_AIPAY_PLATFORM) return normalizePlatform(env.ALIPAY_AIPAY_PLATFORM);
585
+ if (env.AIPAY_AGENT_TYPE) return normalizePlatform(env.AIPAY_AGENT_TYPE);
586
+ if (env.DEV_TOOL_NAME) return normalizePlatform(env.DEV_TOOL_NAME);
587
+ if (env.PLATFORM) return normalizePlatform(env.PLATFORM);
588
+ if (env.CLAUDE_CODE_SESSION_ID || env.CLAUDE_SESSION_ID || env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT) return 'claudecode';
589
+ if (env.OPENCLAW_SESSION_ID || env.OPENCLAW_AGENT_ID) return 'openclaw';
590
+ if (env.CURSOR_TRACE_ID || env.CURSOR_SESSION_ID || env.CURSOR_AGENT || env.CURSOR_LAYOUT) return 'cursor';
591
+ if (env.COZE_PROJECT_ID || env.COZE_SESSION_ID || env.COZE_CONVERSATION_ID) return 'coze';
592
+ if (env.MEOO_PROJECT_ID) return 'meoo';
593
+ if (env.MIAODA) return 'miaoda';
594
+ if (env.CODEX_CI || env.CODEX_THREAD_ID || env.CODEX_SESSION_ID || env.CODEX_SANDBOX_NETWORK_DISABLED !== undefined) return 'codex';
595
+ if (env.TRAE_BRAND_NAME) return 'traesolo';
596
+ if (env.QODER_CLI) return 'qoder';
597
+ return 'unknown';
598
+ }
599
+
600
+ function modelFromEnv(env = process.env) {
601
+ return safeDimension(env.AIPAY_MODEL || 'unknown');
602
+ }
603
+
604
+ function skillVersionFromDisk() {
605
+ try {
606
+ const raw = fs.readFileSync(path.join(skillRoot(), 'VERSION'), 'utf8').trim();
607
+ const match = raw.match(/^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/);
608
+ if (match) return `v${match[1]}`;
609
+ } catch {
610
+ // fall through
611
+ }
612
+ return 'unknown';
613
+ }
614
+
615
+ function isLocalSkillCheckout() {
616
+ try {
617
+ return fs.existsSync(path.join(skillRoot(), '../.git'));
618
+ } catch {
619
+ return false;
620
+ }
621
+ }
622
+
623
+ function machineCodeRaw(env = process.env) {
624
+ const explicit = env.ALIPAY_AIPAY_MACHINE_CODE || env.AIPAY_MACHINE_CODE;
625
+ if (explicit) return explicit;
626
+ try {
627
+ if (process.platform === 'linux') {
628
+ for (const file of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
629
+ if (fs.existsSync(file)) {
630
+ const value = fs.readFileSync(file, 'utf8').trim();
631
+ if (value) return value;
632
+ }
633
+ }
634
+ }
635
+ if (process.platform === 'darwin') {
636
+ const output = execFileSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
637
+ encoding: 'utf8',
638
+ stdio: ['ignore', 'pipe', 'ignore'],
639
+ timeout: 1000
640
+ });
641
+ const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
642
+ if (match?.[1]) return match[1];
643
+ }
644
+ if (process.platform === 'win32') {
645
+ const output = execFileSync('REG', ['QUERY', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid'], {
646
+ encoding: 'utf8',
647
+ stdio: ['ignore', 'pipe', 'ignore'],
648
+ timeout: 1000
649
+ });
650
+ const match = output.match(/MachineGuid\s+REG_SZ\s+([^\s\r\n]+)/);
651
+ if (match?.[1]) return match[1];
652
+ }
653
+ } catch {
654
+ // fall through
655
+ }
656
+ try {
657
+ const hostname = os.hostname();
658
+ if (hostname) return `hostname:${hostname}`;
659
+ } catch {
660
+ // fall through
661
+ }
662
+ return '';
663
+ }
664
+
665
+ function machineUserID(env = process.env) {
666
+ const raw = machineCodeRaw(env);
667
+ if (!raw) return 'anonymous_uid';
668
+ const hash = crypto.createHash('sha256').update(String(raw)).digest('hex').slice(0, 32);
669
+ return `machine_${hash}`;
670
+ }
671
+
672
+ function monitorEnv(env = process.env) {
673
+ switch (normalizeSwitch(env.AIPAY_BUILD_ENV)) {
674
+ case 'dev':
675
+ return 'DEV';
676
+ case 'pre':
677
+ return 'PRE';
678
+ case 'prod':
679
+ return 'PROD';
680
+ default:
681
+ break;
682
+ }
683
+ const skillVersion = skillVersionFromDisk();
684
+ if (/^v?\d+\.\d+\.\d+-/.test(skillVersion)) return 'PRE';
685
+ if (skillVersion !== 'unknown' && !isLocalSkillCheckout()) return 'PROD';
686
+ return 'DEV';
687
+ }
688
+
689
+ export function buildSyncLogReport(event, env = process.env) {
690
+ if (!event) return null;
691
+ const skillVersion = skillVersionFromDisk();
692
+ return {
693
+ item: {
694
+ code: syncLogCode,
695
+ msg: syncLogMsg,
696
+ d1: normalizeOS(),
697
+ d2: platformFromEnv(env),
698
+ d3: modelFromEnv(env),
699
+ d4: skillVersion,
700
+ d5: event.product_type || 'UNKNOWN',
701
+ c1: event.run_id,
702
+ c2: event.stage,
703
+ c3: event.timestamp
704
+ },
705
+ options: {
706
+ Env: monitorEnv(env),
707
+ SprintID: skillVersion,
708
+ UserID: machineUserID(env),
709
+ YuyanID: syncLogYuyanID
710
+ }
711
+ };
712
+ }
713
+
714
+ function serializeSyncLogParam4(param4) {
715
+ return Object.keys(param4)
716
+ .filter((key) => param4[key] !== undefined && param4[key] !== null)
717
+ .sort()
718
+ .map((key) => `${key}=${param4[key]}`)
719
+ .join('^');
720
+ }
721
+
722
+ export function buildSyncLogLine(userID, param4, date = new Date()) {
723
+ const pad = (value, length = 2) => String(value).padStart(length, '0');
724
+ const clientLogTime = [
725
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
726
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}:${date.getMilliseconds()}`
727
+ ].join(' ');
728
+
729
+ return [
730
+ 'D-AE',
731
+ clientLogTime,
732
+ '',
733
+ '',
734
+ '2',
735
+ '',
736
+ '',
737
+ userID || 'anonymous_uid',
738
+ '1000',
739
+ '102023',
740
+ 'H5behavior',
741
+ '2',
742
+ '',
743
+ '',
744
+ '',
745
+ '',
746
+ '',
747
+ '',
748
+ '',
749
+ '',
750
+ '',
751
+ '',
752
+ '',
753
+ 'zh-cn',
754
+ '',
755
+ '',
756
+ '',
757
+ '',
758
+ '',
759
+ '',
760
+ '',
761
+ '',
762
+ '',
763
+ '',
764
+ '',
765
+ '',
766
+ serializeSyncLogParam4(param4)
767
+ ].join(',');
768
+ }
769
+
770
+ function buildParam4(item, options) {
771
+ const param4 = { ...item };
772
+ if (options.SprintID) param4.bm_sid = options.SprintID;
773
+ if (options.Env && !param4.env) param4.env = options.Env;
774
+ if (options.YuyanID) param4.yuyan_id = options.YuyanID;
775
+ param4.monitor_ver = syncLogMonitorVersion;
776
+ return param4;
777
+ }
778
+
779
+ export function syncLogRequestBody(report, nowMs = Date.now()) {
780
+ const param4 = buildParam4(report.item, report.options);
781
+ return `data=${encodeURIComponent(buildSyncLogLine(report.options.UserID || 'anonymous_uid', param4))}&time=${nowMs}`;
782
+ }
783
+
784
+ export function isSuccessfulHttpStatus(statusCode) {
785
+ const code = Number(statusCode || 0);
786
+ return code >= 200 && code < 300;
787
+ }
788
+
789
+ export function sendSyncLogReportDetailed(report, { timeoutMs = uploadTimeoutMs } = {}) {
790
+ return new Promise((resolve) => {
791
+ let settled = false;
792
+ const finish = (result) => {
793
+ if (settled) return;
794
+ settled = true;
795
+ resolve(result);
796
+ };
797
+ try {
798
+ if (!report?.item || !report?.options) {
799
+ finish({ ok: false, error_code: 'INVALID_REPORT', error_message: 'invalid report' });
800
+ return;
801
+ }
802
+ const body = syncLogRequestBody(report);
803
+ const req = https.request(`${syncLogServerURL}?biztype=${syncLogBizType}`, {
804
+ method: 'POST',
805
+ headers: {
806
+ 'Content-Type': 'application/x-www-form-urlencoded',
807
+ 'Content-Length': Buffer.byteLength(body)
808
+ },
809
+ timeout: timeoutMs
810
+ }, (resp) => {
811
+ resp.resume();
812
+ resp.on('end', () => {
813
+ const statusCode = Number(resp.statusCode || 0);
814
+ const ok = isSuccessfulHttpStatus(statusCode);
815
+ finish({
816
+ ok,
817
+ status_code: statusCode || undefined,
818
+ error_code: ok ? undefined : statusCode > 0 ? `HTTP_${statusCode}` : 'NO_HTTP_STATUS',
819
+ error_message: ok ? undefined : statusCode > 0
820
+ ? `sync log endpoint returned ${statusCode}`
821
+ : 'sync log endpoint returned no HTTP status'
822
+ });
823
+ });
824
+ });
825
+ req.on('timeout', () => {
826
+ finish({ ok: false, error_code: 'ETIMEDOUT', error_message: 'request timeout' });
827
+ req.destroy();
828
+ });
829
+ req.on('error', (error) => finish({
830
+ ok: false,
831
+ error_code: safeDimension(error?.code || error?.name || 'REQUEST_ERROR', 'REQUEST_ERROR', 80),
832
+ error_message: safeDimension(error?.message || 'request error', 'request error', 240)
833
+ }));
834
+ req.end(body);
835
+ } catch (error) {
836
+ finish({
837
+ ok: false,
838
+ error_code: safeDimension(error?.code || error?.name || 'REQUEST_ERROR', 'REQUEST_ERROR', 80),
839
+ error_message: safeDimension(error?.message || 'request error', 'request error', 240)
840
+ });
841
+ }
842
+ });
843
+ }
844
+
845
+ export async function sendSyncLogReport(report, { timeoutMs = uploadTimeoutMs } = {}) {
846
+ const result = await sendSyncLogReportDetailed(report, { timeoutMs });
847
+ return result.ok;
848
+ }
849
+
850
+ function outboxFiles(env = process.env) {
851
+ try {
852
+ const dir = outboxDir(env);
853
+ recoverStaleOutboxClaims(env);
854
+ pruneOutbox(env);
855
+ return outboxRecordEntries(dir)
856
+ .sort((left, right) => left.name.localeCompare(right.name))
857
+ .map((entry) => entry.file);
858
+ } catch {
859
+ return [];
860
+ }
861
+ }
862
+
863
+ function outboxRecordEntries(dir) {
864
+ return fs.readdirSync(dir)
865
+ .filter((name) => name.endsWith('.json') && !name.startsWith('.'))
866
+ .map((name) => {
867
+ const file = path.join(dir, name);
868
+ let mtimeMs = 0;
869
+ try {
870
+ mtimeMs = fs.statSync(file).mtimeMs;
871
+ } catch {
872
+ // The file may have been claimed or removed concurrently.
873
+ }
874
+ return { file, name, mtimeMs };
875
+ })
876
+ .filter((entry) => entry.mtimeMs > 0);
877
+ }
878
+
879
+ function pruneOutbox(env = process.env, nowMs = Date.now()) {
880
+ try {
881
+ const dir = outboxDir(env);
882
+ let entries = outboxRecordEntries(dir);
883
+ for (const entry of entries) {
884
+ if (nowMs - entry.mtimeMs <= outboxMaxAgeMs) continue;
885
+ try {
886
+ fs.unlinkSync(entry.file);
887
+ } catch {
888
+ // A concurrent flush may have claimed it first.
889
+ }
890
+ }
891
+ entries = outboxRecordEntries(dir);
892
+ const overflow = entries.length - outboxMaxRecords;
893
+ if (overflow <= 0) return;
894
+ entries
895
+ .sort((left, right) => left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name))
896
+ .slice(0, overflow)
897
+ .forEach((entry) => {
898
+ try {
899
+ fs.unlinkSync(entry.file);
900
+ } catch {
901
+ // Best-effort local retention cap.
902
+ }
903
+ });
904
+ } catch {
905
+ // Pruning is best-effort and must not affect the customer flow.
906
+ }
907
+ }
908
+
909
+ function recoverStaleOutboxClaims(env = process.env, nowMs = Date.now()) {
910
+ try {
911
+ const dir = outboxDir(env);
912
+ for (const name of fs.readdirSync(dir)) {
913
+ if (!name.includes('.json.sending.')) continue;
914
+ const claimedPath = path.join(dir, name);
915
+ const stat = fs.statSync(claimedPath);
916
+ if (nowMs - stat.mtimeMs < outboxClaimTtlMs) continue;
917
+ const originalName = name.replace(/\.sending\.[^.]+$/, '');
918
+ const originalPath = path.join(dir, originalName);
919
+ try {
920
+ if (fs.existsSync(originalPath)) fs.unlinkSync(claimedPath);
921
+ else fs.renameSync(claimedPath, originalPath);
922
+ } catch {
923
+ // Leave the claim for a later recovery attempt.
924
+ }
925
+ }
926
+ } catch {
927
+ // Recovery is best-effort.
928
+ }
929
+ }
930
+
931
+ function claimOutboxFile(file) {
932
+ const claimedPath = `${file}.sending.${process.pid}`;
933
+ try {
934
+ fs.renameSync(file, claimedPath);
935
+ return claimedPath;
936
+ } catch {
937
+ return '';
938
+ }
939
+ }
940
+
941
+ function releaseOutboxClaim(claimedPath) {
942
+ const originalPath = claimedPath.replace(/\.sending\.[^.]+$/, '');
943
+ try {
944
+ if (fs.existsSync(originalPath)) fs.unlinkSync(claimedPath);
945
+ else fs.renameSync(claimedPath, originalPath);
946
+ return true;
947
+ } catch {
948
+ return false;
949
+ }
950
+ }
951
+
952
+ function readOutboxRecord(file) {
953
+ try {
954
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
955
+ if (!record || record.schema_ver !== 1) return null;
956
+ if (record.report?.item && record.report?.options) return record;
957
+ if (record.event?.run_id && record.event?.stage && Number.isFinite(Number(record.event?.timestamp))) return record;
958
+ return null;
959
+ } catch {
960
+ return null;
961
+ }
962
+ }
963
+
964
+ function writeUploadStatus(status, env = process.env) {
965
+ try {
966
+ const statusPath = uploadStatusPath(env);
967
+ ensurePrivateDir(path.dirname(statusPath));
968
+ const next = {
969
+ schema_ver: 1,
970
+ updated_at: Date.now(),
971
+ ...status
972
+ };
973
+ const tempPath = path.join(path.dirname(statusPath), `.${path.basename(statusPath)}.${process.pid}.tmp`);
974
+ fs.writeFileSync(tempPath, `${JSON.stringify(next)}\n`, { mode: 0o600 });
975
+ try {
976
+ fs.chmodSync(tempPath, 0o600);
977
+ } catch {
978
+ // chmod is best-effort on Windows.
979
+ }
980
+ fs.renameSync(tempPath, statusPath);
981
+ return true;
982
+ } catch {
983
+ return false;
984
+ }
985
+ }
986
+
987
+ function readUploadStatus(env = process.env) {
988
+ try {
989
+ const parsed = JSON.parse(fs.readFileSync(uploadStatusPath(env), 'utf8'));
990
+ if (!parsed || parsed.schema_ver !== 1) return null;
991
+ return parsed;
992
+ } catch {
993
+ return null;
994
+ }
995
+ }
996
+
997
+ function readPermissionedFlushState(env = process.env, nowMs = Date.now()) {
998
+ try {
999
+ const parsed = JSON.parse(fs.readFileSync(permissionedFlushStatePath(env), 'utf8'));
1000
+ if (!parsed || parsed.schema_ver !== 1) return null;
1001
+ const requestedAt = Number(parsed.requested_at || 0);
1002
+ if (!Number.isFinite(requestedAt) || nowMs - requestedAt > permissionedFlushTtlMs) return null;
1003
+ return parsed;
1004
+ } catch {
1005
+ return null;
1006
+ }
1007
+ }
1008
+
1009
+ function writePermissionedFlushState(state, env = process.env) {
1010
+ try {
1011
+ const statePath = permissionedFlushStatePath(env);
1012
+ ensurePrivateDir(path.dirname(statePath));
1013
+ const tempPath = path.join(path.dirname(statePath), `.${path.basename(statePath)}.${process.pid}.tmp`);
1014
+ fs.writeFileSync(tempPath, `${JSON.stringify({ schema_ver: 1, ...state })}\n`, { mode: 0o600 });
1015
+ try {
1016
+ fs.chmodSync(tempPath, 0o600);
1017
+ } catch {
1018
+ // chmod is best-effort on Windows.
1019
+ }
1020
+ fs.renameSync(tempPath, statePath);
1021
+ return true;
1022
+ } catch {
1023
+ return false;
1024
+ }
1025
+ }
1026
+
1027
+ function networkUploadFailure(status) {
1028
+ const errorCode = safeDimension(status?.error_code || '', '', 80).toUpperCase();
1029
+ if (permissionedFlushNetworkErrors.has(errorCode)) return errorCode;
1030
+ const message = String(status?.error_message || '').toUpperCase();
1031
+ for (const code of permissionedFlushNetworkErrors) {
1032
+ if (message.includes(code)) return code;
1033
+ }
1034
+ if (message.includes('GETADDRINFO') || message.includes('COULD_NOT_RESOLVE') || message.includes('COULD NOT RESOLVE')) {
1035
+ return 'ENOTFOUND';
1036
+ }
1037
+ return '';
1038
+ }
1039
+
1040
+ export function permissionedFlushDecision(env = process.env, nowMs = Date.now(), { markRequested = false } = {}) {
1041
+ const outboxCount = outboxFiles(env).length;
1042
+ const status = readUploadStatus(env);
1043
+ const errorCode = networkUploadFailure(status);
1044
+ const existing = readPermissionedFlushState(env, nowMs);
1045
+ const alreadyRequested = Boolean(existing);
1046
+ const result = {
1047
+ needed: false,
1048
+ outbox_count: outboxCount,
1049
+ already_requested: alreadyRequested,
1050
+ error_code: errorCode || safeDimension(status?.error_code || '', '', 80),
1051
+ status: safeDimension(status?.status || 'unknown', 'unknown', 80)
1052
+ };
1053
+ if (!telemetryUploadEnabled(env) || outboxCount <= 0 || !errorCode) {
1054
+ return result;
1055
+ }
1056
+ if (alreadyRequested) {
1057
+ return {
1058
+ ...result,
1059
+ requested_at: existing.requested_at
1060
+ };
1061
+ }
1062
+ result.needed = true;
1063
+ if (markRequested) {
1064
+ writePermissionedFlushState({
1065
+ requested_at: nowMs,
1066
+ error_code: errorCode,
1067
+ outbox_count: outboxCount
1068
+ }, env);
1069
+ result.requested_at = nowMs;
1070
+ }
1071
+ return result;
1072
+ }
1073
+
1074
+ export async function flushOutbox(env = process.env, {
1075
+ limit = outboxFlushLimit,
1076
+ timeoutMs = uploadTimeoutMs,
1077
+ sender = sendSyncLogReportDetailed
1078
+ } = {}) {
1079
+ const result = { attempted: 0, sent: 0, retained: 0, invalid: 0 };
1080
+ if (!telemetryUploadEnabled(env)) return result;
1081
+ const files = outboxFiles(env).slice(0, Math.max(0, limit));
1082
+ for (const file of files) {
1083
+ const claimedFile = claimOutboxFile(file);
1084
+ if (!claimedFile) continue;
1085
+ const record = readOutboxRecord(claimedFile);
1086
+ if (!record) {
1087
+ result.invalid += 1;
1088
+ try {
1089
+ fs.unlinkSync(claimedFile);
1090
+ } catch {
1091
+ // best-effort cleanup.
1092
+ }
1093
+ continue;
1094
+ }
1095
+ const report = record.report?.item && record.report?.options ? record.report : buildSyncLogReport(record.event, env);
1096
+ if (!report) {
1097
+ result.invalid += 1;
1098
+ try {
1099
+ fs.unlinkSync(claimedFile);
1100
+ } catch {
1101
+ // best-effort cleanup.
1102
+ }
1103
+ continue;
1104
+ }
1105
+ result.attempted += 1;
1106
+ let sendResult = { ok: false, error_code: 'SENDER_FAILED', error_message: 'sender failed' };
1107
+ try {
1108
+ const rawSendResult = await sender(report, { timeoutMs });
1109
+ sendResult = typeof rawSendResult === 'boolean' ? { ok: rawSendResult } : rawSendResult;
1110
+ } catch (error) {
1111
+ sendResult = {
1112
+ ok: false,
1113
+ error_code: safeDimension(error?.code || error?.name || 'SENDER_THROW', 'SENDER_THROW', 80),
1114
+ error_message: safeDimension(error?.message || 'sender threw', 'sender threw', 240)
1115
+ };
1116
+ }
1117
+ if (sendResult?.ok) {
1118
+ result.sent += 1;
1119
+ try {
1120
+ fs.unlinkSync(claimedFile);
1121
+ } catch {
1122
+ // If delete fails, keep the record for a later retry.
1123
+ result.retained += 1;
1124
+ releaseOutboxClaim(claimedFile);
1125
+ }
1126
+ continue;
1127
+ }
1128
+ writeUploadStatus({
1129
+ status: 'failed',
1130
+ attempted: result.attempted,
1131
+ sent: result.sent,
1132
+ retained: files.length - result.attempted + 1,
1133
+ status_code: sendResult?.status_code,
1134
+ error_code: sendResult?.error_code || 'UPLOAD_FAILED',
1135
+ error_message: sendResult?.error_message || 'upload failed'
1136
+ }, env);
1137
+ releaseOutboxClaim(claimedFile);
1138
+ result.retained += files.length - result.attempted + 1;
1139
+ break;
1140
+ }
1141
+ if (result.sent > 0 && result.retained === 0) {
1142
+ writeUploadStatus({
1143
+ status: 'success',
1144
+ attempted: result.attempted,
1145
+ sent: result.sent,
1146
+ retained: 0
1147
+ }, env);
1148
+ }
1149
+ return result;
1150
+ }
1151
+
1152
+ function spawnOutboxFlush(env = process.env) {
1153
+ if (!telemetryUploadEnabled(env)) return false;
1154
+ try {
1155
+ const child = spawn(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
1156
+ detached: true,
1157
+ stdio: 'ignore',
1158
+ env: {
1159
+ ...env,
1160
+ ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || String(uploadTimeoutMs)
1161
+ }
1162
+ });
1163
+ child.on('error', (error) => {
1164
+ writeUploadStatus({
1165
+ status: 'spawn_failed',
1166
+ error_code: safeDimension(error?.code || error?.name || 'UPLOAD_SPAWN_FAILED', 'UPLOAD_SPAWN_FAILED', 80),
1167
+ error_message: safeDimension(error?.message || 'failed to start background telemetry uploader', 'failed to start background telemetry uploader', 240)
1168
+ }, env);
1169
+ });
1170
+ child.unref();
1171
+ return true;
1172
+ } catch {
1173
+ writeUploadStatus({
1174
+ status: 'spawn_failed',
1175
+ error_code: 'UPLOAD_SPAWN_FAILED',
1176
+ error_message: 'failed to start background telemetry uploader'
1177
+ }, env);
1178
+ return false;
1179
+ }
1180
+ }
1181
+
1182
+ function scheduleSyncLogUpload(event, env = process.env) {
1183
+ if (!telemetryUploadEnabled(env)) return false;
1184
+ if (!enqueueMetricEvent(event, env)) {
1185
+ recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'failed to write local telemetry outbox', env);
1186
+ return false;
1187
+ }
1188
+ return spawnOutboxFlush(env);
1189
+ }
1190
+
1191
+ function emitTelemetryError(error) {
1192
+ try {
1193
+ const message = safeDimension(error instanceof Error ? error.message : String(error), 'unknown', 240);
1194
+ process.stderr.write(`${internalPrefix}TELEMETRY_ERROR=${message}\n`);
1195
+ } catch {
1196
+ // Telemetry must stay nonblocking.
1197
+ }
1198
+ }
1199
+
1200
+ export async function runTelemetryCli(argv = process.argv) {
1201
+ try {
1202
+ const { command, args } = parseCliArgs(argv);
1203
+ if (command === 'upload-status' && args._.length === 0) {
1204
+ assertCliArgs(args, new Set(), 0);
1205
+ const status = readUploadStatus(process.env) || { schema_ver: 1, status: 'unknown' };
1206
+ process.stderr.write(`${internalPrefix}TELEMETRY_UPLOAD_STATUS=${JSON.stringify(status)}\n`);
1207
+ return 0;
1208
+ }
1209
+ if (command === 'permissioned-flush-needed' && args._.length === 0) {
1210
+ assertCliArgs(args, new Set(), 0);
1211
+ const decision = permissionedFlushDecision(process.env, Date.now(), { markRequested: true });
1212
+ process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_DECISION=${JSON.stringify(decision)}\n`);
1213
+ if (decision.needed) {
1214
+ process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_NEEDED=true\n`);
1215
+ }
1216
+ return 0;
1217
+ }
1218
+ if (command === 'flush-outbox') {
1219
+ assertCliArgs(args, new Set(), 0);
1220
+ if (!telemetryEnabled(process.env)) return 0;
1221
+ spawnOutboxFlush(process.env);
1222
+ return 0;
1223
+ }
1224
+ if (command === 'flush-outbox-worker') {
1225
+ assertCliArgs(args, new Set(), 0);
1226
+ if (!telemetryEnabled(process.env)) return 0;
1227
+ const timeout = Number(process.env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || uploadTimeoutMs);
1228
+ await flushOutbox(process.env, { timeoutMs: Number.isFinite(timeout) ? timeout : uploadTimeoutMs });
1229
+ return 0;
1230
+ }
1231
+ if (!telemetryEnabled(process.env)) return 0;
1232
+ if (command === 'start-run') {
1233
+ assertCliArgs(args, new Set(['root-intent', 'product-type', 'run-id']), 0);
1234
+ const state = createOrReuseRunState(args);
1235
+ if (!writeRunState(state)) {
1236
+ recordLocalTelemetryFailure('LOCAL_RUN_STATE_WRITE_FAILED', 'failed to write local telemetry run state');
1237
+ }
1238
+ const event = stageMetricEvent('run.intent_detected', {
1239
+ runId: state.run_id,
1240
+ rootIntent: state.root_intent,
1241
+ productType: state.product_type
1242
+ });
1243
+ emitMetricEvent(event);
1244
+ return 0;
1245
+ }
1246
+ if (command === 'current-run-id') {
1247
+ assertCliArgs(args, new Set(), 0);
1248
+ const state = canReadImplicitRunState(process.env) ? readRunState(process.env) : null;
1249
+ if (state?.run_id) {
1250
+ process.stderr.write(`${internalPrefix}RUN_ID=${state.run_id}\n`);
1251
+ } else {
1252
+ process.stderr.write(`${internalPrefix}RUN_ID_MISSING=true\n`);
1253
+ }
1254
+ return 0;
1255
+ }
1256
+ if (command !== 'stage' || args._.length !== 1) {
1257
+ emitTelemetryError(new Error('invalid telemetry arguments'));
1258
+ return 0;
1259
+ }
1260
+ assertCliArgs(args, new Set(['run-id', 'root-intent', 'product-type', 'product-name']), 1);
1261
+ const event = stageMetricEvent(args._[0], contextFromCliArgs(args));
1262
+ if (event) updateExistingRunStateFromEvent(event);
1263
+ emitMetricEvent(event);
1264
+ return 0;
1265
+ } catch (error) {
1266
+ emitTelemetryError(error);
1267
+ return 0;
1268
+ }
1269
+ }
1270
+
1271
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
1272
+ runTelemetryCli(process.argv).then((code) => {
1273
+ process.exitCode = code;
1274
+ }, (error) => {
1275
+ emitTelemetryError(error);
1276
+ process.exitCode = 0;
1277
+ });
1278
+ }