@alipay/alipay-aipay 1.6.2-beta → 1.6.2-beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alipay/alipay-aipay",
3
- "version": "1.6.2-beta",
3
+ "version": "1.6.2-beta2",
4
4
  "description": "支付宝 AI 付 Skill 安装包 - 优先安装 Agent Skill,并在 macOS/Linux 后台准备用户级 CLI 工具",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -5,27 +5,23 @@ description: >-
5
5
  触发关键词:"AI支付"、"接支付宝"、"一站式接支付宝"、"接个支付"、"开发支付"、"402收款"、"AI按量付费"、"AI网页应用收款"、"AI移动应用收款"、"按量付费"、"快捷收款"、"快捷收单"、"网站支付"、"APP 支付"、"产品开通"、"商家开通"、"签约入驻"等。
6
6
  ---
7
7
 
8
- # 支付宝 AI 付 Skill 启动器
8
+ 本文件只负责在加载业务规则前确定本轮实际使用的 Skill 版本。禁止在完成下列启动协议前读取 `references/runtime-entry.md`、flow、模块、消息目录或业务脚本。
9
9
 
10
- 本文件只负责在加载业务规则前确定本轮实际使用的 Skill 版本。禁止在完成下列启动协议前读取 `references/runtime-entry.md`、任一 flow、模块、消息目录或业务脚本。
11
-
12
- ## 前置版本检查
13
-
14
- 本次 Agent 对话首次使用 `alipay-aipay` 时,先把本文件所在目录解析为规范化绝对路径 `<SKILL_DIR>`,记录一次性事实 `ALIPAY_AIPAY_SELF_UPDATE_CHECKED=true`,再执行以下单条命令。`<SKILL_DIR>` 是执行时替换的路径占位符;禁止用 shell 变量赋值、命令替换或依赖特定 shell:
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` 必须绑定刚触发且正在执行本命令的 Skill 规范化绝对路径,不得换成 Agent 启动目录、用户项目目录、用户 Home、npm 包源码根或另一份 Skill 副本。`VERSION` 缺失/非法或 `references/runtime-entry.md` 缺失时由本命令尝试精确修复;命令失败或 marker 无法唯一解析时按 `SELF_UPDATE:CHECK_FAILED`。
16
+ `SKILL_DIR` 只绑定当前 Skill,禁止换成 Agent 启动目录、用户项目、Home、npm 源码根或其他副本。`VERSION` 缺失/非法或入口缺失由命令修复;失败或 marker 不唯一按 `SELF_UPDATE:CHECK_FAILED`。
21
17
 
22
- stderr 只允许出现以下一个内部终态;不得向用户展示 marker 或 npm/npx 原始输出:
18
+ stderr 只允许一个内部终态,禁止展示 marker 或 npm/npx 原始输出:
23
19
 
24
- - `SELF_UPDATE:UPDATED=<合法 SemVer>`:当前 Skill 目录已原子切换到新版。
20
+ - `SELF_UPDATE:UPDATED=<合法 SemVer>`:已切换新版。
25
21
  - `SELF_UPDATE:CURRENT=<合法 SemVer>`:继续当前版本。
26
- - `SELF_UPDATE:RETRY_WITH_NETWORK`:registry 或安装阶段网络不可达;可申请联网后原样重试本命令一次。
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` 的一次原样重试外,只完整读取同一 `SKILL_DIR` 的 `references/runtime-entry.md`,在当前上下文记录 `ALIPAY_AIPAY_RUNTIME_ENTRY_LOADED=true` 和规范化路径,再按原请求路由。`UPDATED` 后先废弃旧 loaded 事实,禁止继续读取更新前版本的任何业务文档;其他终态同样不再读取本启动器或 `self-update.md` 补充规则。
25
+ 除 `RETRY_WITH_NETWORK` 外,只完整读取同一 `SKILL_DIR` 的 `references/runtime-entry.md`,记录 `ALIPAY_AIPAY_RUNTIME_ENTRY_LOADED=true` 和路径,再路由。`UPDATED` 后废弃旧 loaded 事实,禁止继续读取更新前版本的任何业务文档;其他终态同样不再读取本启动器或 `self-update.md` 补充规则。
30
26
 
31
- 同一对话后续确认、补材料、候选选择、恢复、中断继续、重复触发和 Full Process 衔接,两项事实存在、路径未变且入口规则仍在时,跳过版本检查和入口重读。仅 `UPDATED`、路径变化或上下文压缩导致规则不可用时重读并刷新;事实不落盘。
27
+ 同一对话后续确认、补材料、候选、恢复、中断继续、重复触发和 Full Process 衔接,两项事实存在且路径未变时跳过版本检查和入口重读;仅 `UPDATED`、路径变化或上下文压缩导致规则不可用时重读并刷新;事实不落盘。
package/skill/VERSION CHANGED
@@ -1 +1 @@
1
- 1.6.2-beta
1
+ 1.6.2-beta2
@@ -18,6 +18,7 @@ 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;
21
22
  const outboxFlushLimit = 50;
22
23
  const outboxClaimTtlMs = 5 * 60 * 1000;
23
24
  const outboxMaxRecords = 1000;
@@ -119,6 +120,14 @@ function safeDimension(value, fallback = 'unknown', maxLength = 80) {
119
120
  return trimmed.slice(0, maxLength);
120
121
  }
121
122
 
123
+ function failedResult(error, code = 'REQUEST_ERROR', message = 'request error') {
124
+ return {
125
+ ok: false,
126
+ error_code: safeDimension(error?.code || error?.name || code, code, 80),
127
+ error_message: safeDimension(error?.message || message, message, 240)
128
+ };
129
+ }
130
+
122
131
  function normalizeSwitch(value) {
123
132
  return String(value || '').trim().toLowerCase();
124
133
  }
@@ -224,8 +233,21 @@ function ensurePrivateDir(dir) {
224
233
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
225
234
  try {
226
235
  fs.chmodSync(dir, 0o700);
236
+ } catch {}
237
+ }
238
+
239
+ function writePrivateJson(file, value) {
240
+ try {
241
+ ensurePrivateDir(path.dirname(file));
242
+ const tempPath = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.tmp`);
243
+ fs.writeFileSync(tempPath, `${JSON.stringify(value)}\n`, { mode: 0o600 });
244
+ try {
245
+ fs.chmodSync(tempPath, 0o600);
246
+ } catch {}
247
+ fs.renameSync(tempPath, file);
248
+ return true;
227
249
  } catch {
228
- // chmod is best-effort on Windows.
250
+ return false;
229
251
  }
230
252
  }
231
253
 
@@ -273,21 +295,7 @@ function recordLocalTelemetryFailure(errorCode, message, env = process.env) {
273
295
  }
274
296
 
275
297
  function writeRunState(state, env = process.env) {
276
- try {
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
- }
298
+ return writePrivateJson(runStatePath(env), state);
291
299
  }
292
300
 
293
301
  function outboxFilename(event) {
@@ -296,36 +304,31 @@ function outboxFilename(event) {
296
304
  return `${timestamp}-${eventID}.json`;
297
305
  }
298
306
 
299
- export function enqueueMetricEvent(event, env = process.env) {
307
+ function writeOutboxRecord(event, env = process.env) {
300
308
  try {
301
- if (!event) return false;
309
+ if (!event) return '';
302
310
  const dir = outboxDir(env);
303
311
  ensurePrivateDir(dir);
304
312
  const filename = outboxFilename(event);
305
313
  const finalPath = path.join(dir, filename);
306
- const tempPath = path.join(dir, `.${filename}.${process.pid}.tmp`);
307
- const record = {
314
+ if (!writePrivateJson(finalPath, {
308
315
  schema_ver: 1,
309
316
  created_at: Date.now(),
310
317
  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);
318
+ })) return '';
319
319
  pruneOutbox(env);
320
- return true;
320
+ return finalPath;
321
321
  } catch {
322
- return false;
322
+ return '';
323
323
  }
324
324
  }
325
325
 
326
+ export function enqueueMetricEvent(event, env = process.env) {
327
+ return Boolean(writeOutboxRecord(event, env));
328
+ }
329
+
326
330
  function safeRunId(value) {
327
331
  if (typeof value !== 'string' || !value.trim()) return '';
328
- // Run IDs also participate in state keys and param4 matching, so keep them separator-free.
329
332
  return /[\u0000-\u001f\u007f,^=]/u.test(value) ? '' : value;
330
333
  }
331
334
 
@@ -441,6 +444,7 @@ export function stageMetricEvent(stage, context = {}, nowMs = Date.now(), env =
441
444
  schema_ver: 2,
442
445
  event_id: crypto.randomUUID(),
443
446
  skill: 'alipay-aipay',
447
+ skill_version: skillVersionFromDisk(),
444
448
  run_id: runId,
445
449
  stage: rule.stage,
446
450
  timestamp: nowMs,
@@ -745,15 +749,20 @@ function modelFromEnv(env = process.env) {
745
749
 
746
750
  function skillVersionFromDisk() {
747
751
  try {
748
- const raw = fs.readFileSync(path.join(skillRoot(), 'VERSION'), 'utf8').trim();
749
- const match = raw.match(/^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/);
750
- if (match) return `v${match[1]}`;
751
- } catch {
752
- // fall through
753
- }
752
+ return normalizeSkillVersion(fs.readFileSync(path.join(skillRoot(), 'VERSION'), 'utf8')) || 'unknown';
753
+ } catch {}
754
754
  return 'unknown';
755
755
  }
756
756
 
757
+ function normalizeSkillVersion(value) {
758
+ const match = String(value || '').trim().match(/^v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/);
759
+ return match ? `v${match[1]}` : '';
760
+ }
761
+
762
+ function skillVersionFromEvent(event) {
763
+ return normalizeSkillVersion(event?.skill_version) || skillVersionFromDisk();
764
+ }
765
+
757
766
  function isLocalSkillCheckout() {
758
767
  try {
759
768
  return fs.existsSync(path.join(skillRoot(), '../.git'));
@@ -792,15 +801,11 @@ function machineCodeRaw(env = process.env) {
792
801
  const match = output.match(/MachineGuid\s+REG_SZ\s+([^\s\r\n]+)/);
793
802
  if (match?.[1]) return match[1];
794
803
  }
795
- } catch {
796
- // fall through
797
- }
804
+ } catch {}
798
805
  try {
799
806
  const hostname = os.hostname();
800
807
  if (hostname) return `hostname:${hostname}`;
801
- } catch {
802
- // fall through
803
- }
808
+ } catch {}
804
809
  return '';
805
810
  }
806
811
 
@@ -811,7 +816,7 @@ function machineUserID(env = process.env) {
811
816
  return `machine_${hash}`;
812
817
  }
813
818
 
814
- function monitorEnv(env = process.env) {
819
+ function monitorEnv(env = process.env, skillVersion = skillVersionFromDisk()) {
815
820
  switch (normalizeSwitch(env.AIPAY_BUILD_ENV)) {
816
821
  case 'dev':
817
822
  return 'DEV';
@@ -822,7 +827,6 @@ function monitorEnv(env = process.env) {
822
827
  default:
823
828
  break;
824
829
  }
825
- const skillVersion = skillVersionFromDisk();
826
830
  if (/^v?\d+\.\d+\.\d+-/.test(skillVersion)) return 'PRE';
827
831
  if (skillVersion !== 'unknown' && !isLocalSkillCheckout()) return 'PROD';
828
832
  return 'DEV';
@@ -830,7 +834,7 @@ function monitorEnv(env = process.env) {
830
834
 
831
835
  export function buildSyncLogReport(event, env = process.env) {
832
836
  if (!event) return null;
833
- const skillVersion = skillVersionFromDisk();
837
+ const skillVersion = skillVersionFromEvent(event);
834
838
  const item = {
835
839
  code: syncLogCode,
836
840
  msg: syncLogMsg,
@@ -848,7 +852,7 @@ export function buildSyncLogReport(event, env = process.env) {
848
852
  return {
849
853
  item,
850
854
  options: {
851
- Env: monitorEnv(env),
855
+ Env: monitorEnv(env, skillVersion),
852
856
  SprintID: skillVersion,
853
857
  UserID: machineUserID(env),
854
858
  YuyanID: syncLogYuyanID
@@ -971,8 +975,8 @@ export function sendSyncLogReportDetailed(report, { timeoutMs = uploadTimeoutMs
971
975
  status_code: statusCode || undefined,
972
976
  error_code: ok ? undefined : statusCode > 0 ? `HTTP_${statusCode}` : 'NO_HTTP_STATUS',
973
977
  error_message: ok ? undefined : statusCode > 0
974
- ? `sync log endpoint returned ${statusCode}`
975
- : 'sync log endpoint returned no HTTP status'
978
+ ? `http ${statusCode}`
979
+ : 'no http status'
976
980
  });
977
981
  });
978
982
  });
@@ -980,18 +984,10 @@ export function sendSyncLogReportDetailed(report, { timeoutMs = uploadTimeoutMs
980
984
  finish({ ok: false, error_code: 'ETIMEDOUT', error_message: 'request timeout' });
981
985
  req.destroy();
982
986
  });
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
- }));
987
+ req.on('error', (error) => finish(failedResult(error)));
988
988
  req.end(body);
989
989
  } 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
- });
990
+ finish(failedResult(error));
995
991
  }
996
992
  });
997
993
  }
@@ -1001,14 +997,25 @@ export async function sendSyncLogReport(report, { timeoutMs = uploadTimeoutMs }
1001
997
  return result.ok;
1002
998
  }
1003
999
 
1004
- function outboxFiles(env = process.env) {
1000
+ function outboxFiles(env = process.env, { targetFile = '', limit = 0 } = {}) {
1005
1001
  try {
1006
1002
  const dir = outboxDir(env);
1007
1003
  recoverStaleOutboxClaims(env);
1008
1004
  pruneOutbox(env);
1009
- return outboxRecordEntries(dir)
1005
+ if (targetFile) {
1006
+ const resolved = path.resolve(String(targetFile));
1007
+ const basename = path.basename(resolved);
1008
+ return path.dirname(resolved) === path.resolve(dir)
1009
+ && basename.endsWith('.json')
1010
+ && !basename.startsWith('.')
1011
+ && fs.existsSync(resolved)
1012
+ ? [resolved]
1013
+ : [];
1014
+ }
1015
+ const files = outboxRecordEntries(dir)
1010
1016
  .sort((left, right) => left.name.localeCompare(right.name))
1011
1017
  .map((entry) => entry.file);
1018
+ return limit > 0 ? files.slice(0, limit) : files;
1012
1019
  } catch {
1013
1020
  return [];
1014
1021
  }
@@ -1022,9 +1029,7 @@ function outboxRecordEntries(dir) {
1022
1029
  let mtimeMs = 0;
1023
1030
  try {
1024
1031
  mtimeMs = fs.statSync(file).mtimeMs;
1025
- } catch {
1026
- // The file may have been claimed or removed concurrently.
1027
- }
1032
+ } catch {}
1028
1033
  return { file, name, mtimeMs };
1029
1034
  })
1030
1035
  .filter((entry) => entry.mtimeMs > 0);
@@ -1038,9 +1043,7 @@ function pruneOutbox(env = process.env, nowMs = Date.now()) {
1038
1043
  if (nowMs - entry.mtimeMs <= outboxMaxAgeMs) continue;
1039
1044
  try {
1040
1045
  fs.unlinkSync(entry.file);
1041
- } catch {
1042
- // A concurrent flush may have claimed it first.
1043
- }
1046
+ } catch {}
1044
1047
  }
1045
1048
  entries = outboxRecordEntries(dir);
1046
1049
  const overflow = entries.length - outboxMaxRecords;
@@ -1051,13 +1054,9 @@ function pruneOutbox(env = process.env, nowMs = Date.now()) {
1051
1054
  .forEach((entry) => {
1052
1055
  try {
1053
1056
  fs.unlinkSync(entry.file);
1054
- } catch {
1055
- // Best-effort local retention cap.
1056
- }
1057
+ } catch {}
1057
1058
  });
1058
- } catch {
1059
- // Pruning is best-effort and must not affect the customer flow.
1060
- }
1059
+ } catch {}
1061
1060
  }
1062
1061
 
1063
1062
  function recoverStaleOutboxClaims(env = process.env, nowMs = Date.now()) {
@@ -1073,13 +1072,9 @@ function recoverStaleOutboxClaims(env = process.env, nowMs = Date.now()) {
1073
1072
  try {
1074
1073
  if (fs.existsSync(originalPath)) fs.unlinkSync(claimedPath);
1075
1074
  else fs.renameSync(claimedPath, originalPath);
1076
- } catch {
1077
- // Leave the claim for a later recovery attempt.
1078
- }
1075
+ } catch {}
1079
1076
  }
1080
- } catch {
1081
- // Recovery is best-effort.
1082
- }
1077
+ } catch {}
1083
1078
  }
1084
1079
 
1085
1080
  function claimOutboxFile(file) {
@@ -1116,26 +1111,7 @@ function readOutboxRecord(file) {
1116
1111
  }
1117
1112
 
1118
1113
  function writeUploadStatus(status, env = process.env) {
1119
- try {
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
- }
1114
+ return writePrivateJson(uploadStatusPath(env), { schema_ver: 1, updated_at: Date.now(), ...status });
1139
1115
  }
1140
1116
 
1141
1117
  function readUploadStatus(env = process.env) {
@@ -1161,21 +1137,7 @@ function readPermissionedFlushState(env = process.env, nowMs = Date.now()) {
1161
1137
  }
1162
1138
 
1163
1139
  function writePermissionedFlushState(state, env = process.env) {
1164
- try {
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
- }
1140
+ return writePrivateJson(permissionedFlushStatePath(env), { schema_ver: 1, ...state });
1179
1141
  }
1180
1142
 
1181
1143
  function networkUploadFailure(status) {
@@ -1227,12 +1189,13 @@ export function permissionedFlushDecision(env = process.env, nowMs = Date.now(),
1227
1189
 
1228
1190
  export async function flushOutbox(env = process.env, {
1229
1191
  limit = outboxFlushLimit,
1192
+ targetFile = '',
1230
1193
  timeoutMs = uploadTimeoutMs,
1231
1194
  sender = sendSyncLogReportDetailed
1232
1195
  } = {}) {
1233
1196
  const result = { attempted: 0, sent: 0, retained: 0, invalid: 0 };
1234
1197
  if (!telemetryUploadEnabled(env)) return result;
1235
- const files = outboxFiles(env).slice(0, Math.max(0, limit));
1198
+ const files = outboxFiles(env, { targetFile, limit });
1236
1199
  for (const file of files) {
1237
1200
  const claimedFile = claimOutboxFile(file);
1238
1201
  if (!claimedFile) continue;
@@ -1241,9 +1204,7 @@ export async function flushOutbox(env = process.env, {
1241
1204
  result.invalid += 1;
1242
1205
  try {
1243
1206
  fs.unlinkSync(claimedFile);
1244
- } catch {
1245
- // best-effort cleanup.
1246
- }
1207
+ } catch {}
1247
1208
  continue;
1248
1209
  }
1249
1210
  const report = record.report?.item && record.report?.options ? record.report : buildSyncLogReport(record.event, env);
@@ -1251,9 +1212,7 @@ export async function flushOutbox(env = process.env, {
1251
1212
  result.invalid += 1;
1252
1213
  try {
1253
1214
  fs.unlinkSync(claimedFile);
1254
- } catch {
1255
- // best-effort cleanup.
1256
- }
1215
+ } catch {}
1257
1216
  continue;
1258
1217
  }
1259
1218
  result.attempted += 1;
@@ -1262,18 +1221,13 @@ export async function flushOutbox(env = process.env, {
1262
1221
  const rawSendResult = await sender(report, { timeoutMs });
1263
1222
  sendResult = typeof rawSendResult === 'boolean' ? { ok: rawSendResult } : rawSendResult;
1264
1223
  } 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
- };
1224
+ sendResult = failedResult(error, 'SENDER_THROW', 'sender threw');
1270
1225
  }
1271
1226
  if (sendResult?.ok) {
1272
1227
  result.sent += 1;
1273
1228
  try {
1274
1229
  fs.unlinkSync(claimedFile);
1275
1230
  } catch {
1276
- // If delete fails, keep the record for a later retry.
1277
1231
  result.retained += 1;
1278
1232
  releaseOutboxClaim(claimedFile);
1279
1233
  }
@@ -1318,7 +1272,7 @@ function spawnOutboxFlush(env = process.env) {
1318
1272
  writeUploadStatus({
1319
1273
  status: 'spawn_failed',
1320
1274
  error_code: safeDimension(error?.code || error?.name || 'UPLOAD_SPAWN_FAILED', 'UPLOAD_SPAWN_FAILED', 80),
1321
- error_message: safeDimension(error?.message || 'failed to start background telemetry uploader', 'failed to start background telemetry uploader', 240)
1275
+ error_message: safeDimension(error?.message || 'upload spawn failed', 'upload spawn failed', 240)
1322
1276
  }, env);
1323
1277
  });
1324
1278
  child.unref();
@@ -1327,7 +1281,32 @@ function spawnOutboxFlush(env = process.env) {
1327
1281
  writeUploadStatus({
1328
1282
  status: 'spawn_failed',
1329
1283
  error_code: 'UPLOAD_SPAWN_FAILED',
1330
- error_message: 'failed to start background telemetry uploader'
1284
+ error_message: 'upload spawn failed'
1285
+ }, env);
1286
+ return false;
1287
+ }
1288
+ }
1289
+
1290
+ function foregroundFlushTerminalOutboxFile(file, env = process.env) {
1291
+ if (!telemetryUploadEnabled(env) || !file) return false;
1292
+ const parsed = parseNonNegativeInteger(env.ALIPAY_AIPAY_TELEMETRY_TERMINAL_FLUSH_TIMEOUT_MS);
1293
+ const timeoutMs = parsed && parsed > 0 ? Math.min(parsed, 30000) : terminalForegroundUploadTimeoutMs;
1294
+ try {
1295
+ execFileSync(process.execPath, [scriptPath(), 'flush-outbox-worker'], {
1296
+ stdio: 'ignore',
1297
+ timeout: timeoutMs + 800,
1298
+ env: {
1299
+ ...env,
1300
+ ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS: String(timeoutMs),
1301
+ ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE: file
1302
+ }
1303
+ });
1304
+ return true;
1305
+ } catch (error) {
1306
+ writeUploadStatus({
1307
+ status: 'failed',
1308
+ error_code: safeDimension(error?.code || error?.signal || error?.name || 'TERMINAL_FLUSH_FAILED', 'TERMINAL_FLUSH_FAILED', 80),
1309
+ error_message: 'terminal flush failed'
1331
1310
  }, env);
1332
1311
  return false;
1333
1312
  }
@@ -1335,10 +1314,15 @@ function spawnOutboxFlush(env = process.env) {
1335
1314
 
1336
1315
  function scheduleSyncLogUpload(event, env = process.env) {
1337
1316
  if (!telemetryUploadEnabled(env)) return false;
1338
- if (!enqueueMetricEvent(event, env)) {
1339
- recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'failed to write local telemetry outbox', env);
1317
+ const queuedFile = writeOutboxRecord(event, env);
1318
+ if (!queuedFile) {
1319
+ recordLocalTelemetryFailure('LOCAL_OUTBOX_WRITE_FAILED', 'outbox write failed', env);
1340
1320
  return false;
1341
1321
  }
1322
+ if (event?.stage === 'run.terminal') {
1323
+ foregroundFlushTerminalOutboxFile(queuedFile, env);
1324
+ return true;
1325
+ }
1342
1326
  return spawnOutboxFlush(env);
1343
1327
  }
1344
1328
 
@@ -1347,7 +1331,6 @@ function emitTelemetryError(error) {
1347
1331
  const message = safeDimension(error instanceof Error ? error.message : String(error), 'unknown', 240);
1348
1332
  process.stderr.write(`${internalPrefix}TELEMETRY_ERROR=${message}\n`);
1349
1333
  } catch {
1350
- // Telemetry must stay nonblocking.
1351
1334
  }
1352
1335
  }
1353
1336
 
@@ -1386,6 +1369,15 @@ export async function runTelemetryCli(argv = process.argv) {
1386
1369
  process.stderr.write(`${internalPrefix}TELEMETRY_UPLOAD_STATUS=${JSON.stringify(status)}\n`);
1387
1370
  return 0;
1388
1371
  }
1372
+ if (command === 'permissioned-flush-status' && args._.length === 0) {
1373
+ assertCliArgs(args, new Set(), 0);
1374
+ const decision = permissionedFlushDecision(process.env, Date.now(), { markRequested: false });
1375
+ process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_STATUS=${JSON.stringify(decision)}\n`);
1376
+ if (decision.needed) {
1377
+ process.stderr.write(`${internalPrefix}PERMISSIONED_FLUSH_CANDIDATE=true\n`);
1378
+ }
1379
+ return 0;
1380
+ }
1389
1381
  if (command === 'permissioned-flush-needed' && args._.length === 0) {
1390
1382
  assertCliArgs(args, new Set(), 0);
1391
1383
  const decision = permissionedFlushDecision(process.env, Date.now(), { markRequested: true });
@@ -1405,7 +1397,10 @@ export async function runTelemetryCli(argv = process.argv) {
1405
1397
  assertCliArgs(args, new Set(), 0);
1406
1398
  if (!telemetryEnabled(process.env)) return 0;
1407
1399
  const timeout = Number(process.env.ALIPAY_AIPAY_TELEMETRY_UPLOAD_TIMEOUT_MS || uploadTimeoutMs);
1408
- await flushOutbox(process.env, { timeoutMs: Number.isFinite(timeout) ? timeout : uploadTimeoutMs });
1400
+ await flushOutbox(process.env, {
1401
+ targetFile: process.env.ALIPAY_AIPAY_TELEMETRY_FLUSH_TARGET_FILE || '',
1402
+ timeoutMs: Number.isFinite(timeout) ? timeout : uploadTimeoutMs
1403
+ });
1409
1404
  return 0;
1410
1405
  }
1411
1406
  if (!telemetryEnabled(process.env)) return 0;
@@ -1,5 +1,4 @@
1
- - stage/Yuyan 低敏维度以 `scripts/telemetry.mjs` 与测试白名单为准;只含阶段、产品、根意图、时间戳、版本、宿主/模型、匿名机器哈希、run_id、终态计数。禁密钥、账号密码、订单/交易号、截图/凭证、用户输入、路径/URL、响应、命令输出/堆栈原文。遥测失败不影响业务;内部行 `ALIPAY_AIPAY_INTERNAL:`。
2
- - `start-run` 创建/复用 `run_id`;脚本继承 `PLATFORM_ID=<run_id>`,Agent 不手写环境前缀或 MCP payload。outbox 在系统临时目录,collector 2xx 后才删;维护/测试/Benchmark 禁上传。
3
- - run 复用:`root_intent + product_type` 判定复用/新建;`UNKNOWN` 补全产品仍复用同一 `run_id`。仅换产品、换根意图或已有终态才重建;“重新开始/重新走流程/作废本轮流程”但组合未变仍复用。`COZE_PROJECT_ID`、`MEOO_PROJECT_ID`、`VITE_APP_ID`/`PLATFORM_ID` 优先作新 run ID。`current-run-id` 只查看,不新建 run。
4
- - 计数:`user_input_count_total` 从触发当前 `run_id` 的用户消息起;`user_input_count_after_flow_start` 从方案送达并进入正式执行后的下一条用户消息起(`integration_only/full_process=integration.user_confirmed`,`onboarding_only=onboarding.user_confirmed`)。产品/MCC 澄清或选择、项目/语言/框架补齐、项目候选和方案本身只计 total;`full_process` 不因 Onboarding 重置。`flow_start_observed` 表示正式 flow start;用户 kill 未终态不补报。终态只加三项计数参数,不新增业务确认点、stage 或独立上报。`c4=ext_info`,计数合法且有 `root_intent` 才写 JSON,否则省略。
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`;换产品/根意图/已终态/重新开始/重新走流程/作废本轮流程才新建。第三方 ID(`COZE_PROJECT_ID`、`MEOO_PROJECT_ID`、`VITE_APP_ID`/`PLATFORM_ID`)优先;`current-run-id` 只查看,不新建 run。outbox 落盘,collector 2xx 才删;维护/测试/Benchmark 禁上传。
3
+ - 计数:`user_input_count_total` 自触发当前 run 的用户消息起;`user_input_count_after_flow_start` 自方案送达并进入正式执行后的下一条用户消息起。产品/MCC 澄清或选择、项目/语言/框架补齐、项目候选和方案本身只计 total;`full_process` 不因 Onboarding 重置。`flow_start_observed` 表示正式 flow start;用户 kill 未终态不补报。终态只追加计数,不新增确认点、stage 或独立上报。`run.terminal` 入队后以短超时前台 drain,仅当前 terminal 文件,避免流程最后的 detached worker 被宿主回收,不申请额外权限;失败仍保留 outbox。`c4=ext_info`,计数合法且有 `root_intent` 才写 JSON。
4
+ - 补传:不为权限探测新增网络事件;实际低敏上传就是自然探测,宿主已允许访问 collector 时不额外申请权限。`upload-status`/`permissioned-flush-status` 只读;后者输出 `PERMISSIONED_FLUSH_CANDIDATE=true`(最近 `ENOTFOUND|EAI_AGAIN|ETIMEDOUT`、outbox 积压、12h 未请求)后执行 `permissioned-flush-needed` 预占窗口。唯一 `PERMISSIONED_FLUSH_NEEDED=true` 时才申请 `telemetry flush-outbox`;`flush-outbox` 后台补传:POST `https://collect.alipay.com/yuyan/`(`collect.alipay.com:443`,路径 `/yuyan/`)。说明:`是否允许 Agent 上报 Skill 使用过程的统计信息?只含产品类别、时间、Skill 版本、匿名设备标识、人机交互次数等用于帮助改善用户体验的非敏感信息,不会采集包含源代码、用户输入内容等任何敏感信息。` 同一 Agent 会话只请求一次;拒绝/失败/非 2xx 保留 outbox;collector 结果非业务条件;无 `inspect-outbox`、`--confirm-reviewed`、`alipay-cli whoami` 兜底。
@@ -48,7 +48,7 @@ node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" render <messageId> --va
48
48
 
49
49
  `full_process` 识别根意图后立即执行 `node "<SKILL_DIR>/references/normal/scripts/runtime.mjs" telemetry start-run --root-intent full_process`;产品已唯一确定时追加 `--product-type <aipay|webpay|apppay>`。`integration_only` 和 `onboarding_only` 已由上表各自 bootstrap 包含同一次 `start-run`,不得重复执行本条 telemetry。命令创建/复用当前流程 `run_id`,并在遥测开启时记录 `run.intent_detected`:同一意图和产品继续复用,`UNKNOWN` 补全产品仍复用同一 `run_id`;明确换产品、换根意图或既有 run 已终态才重建。第三方平台项目 ID 仍优先固定为 `run_id`,不得被显式 run ID 或本地生成值覆盖。业务 stage 只由登记 runner/script 异步记录。
50
50
 
51
- 遥测默认开启,`ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 可关闭 `METRIC_EVENT`、outbox、上传和 flush;私有流程 run context 仍用于 `PLATFORM_ID` 和终态生命周期。落盘/网络/上传/flush 失败静默,不改变业务、消息或确认,stdout 为空且 stderr 内部行不对客。`integration_only` 由 checklist 终结;Onboarding 由 closeout 或登记的提前终止消息终结。Agent 按 `normal/telemetry.md` 维护本轮用户输入计数,并只在终态命令追加可选参数,不新增业务确认点或独立上报。after-flow 以方案后 `*.user_confirmed` 为起点;前置澄清和候选选择不计入。脚本始终以当前流程 `run_id` 为 `PLATFORM_ID`,Agent 不手写环境前缀或改 MCP payload;维护诊断、outbox、上传和 flush 细节只在 `normal/telemetry.md` 按需读取。
51
+ 遥测默认开启,`ALIPAY_AIPAY_TELEMETRY=0|false|off|no` 可关闭 `METRIC_EVENT`、outbox、上传和 flush;私有 run context 仍供 `PLATFORM_ID` 与终态生命周期使用。落盘/网络/上传/flush 失败静默,不改变业务、消息或确认,stdout 为空且 stderr 内部行不对客。`integration_only` 由 checklist 终结;Onboarding 由 closeout 或登记的提前终止消息终结。上报权限不新增专门网络探针,首次 `start-run` 及后续 stage 的实际低敏上传就是自然探测;宿主已允许访问 collector 时不额外申请权限,仅 `normal/telemetry.md` 的只读状态命令确认网络失败和 outbox 积压后,才允许一次补传权限申请。Agent 按 `normal/telemetry.md` 维护本轮用户输入计数,只在终态命令追加参数,不新增业务确认点或独立上报;入队后当前 `run.terminal` 文件做短超时前台 drain,失败仍保留 outbox 且不影响业务收口。after-flow 以方案后 `*.user_confirmed` 为起点;前置澄清和候选选择不计入。脚本始终以当前 `run_id` 为 `PLATFORM_ID`,Agent 不手写环境前缀或改 MCP payload;维护诊断、outbox、上传和 flush 细节只在 `normal/telemetry.md` 按需读取。
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