@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
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-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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
307
|
+
function writeOutboxRecord(event, env = process.env) {
|
|
300
308
|
try {
|
|
301
|
-
if (!event) return
|
|
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
|
-
|
|
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
|
|
320
|
+
return finalPath;
|
|
321
321
|
} catch {
|
|
322
|
-
return
|
|
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
|
-
|
|
749
|
-
|
|
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 =
|
|
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
|
-
? `
|
|
975
|
-
: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 || '
|
|
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: '
|
|
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
|
-
|
|
1339
|
-
|
|
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, {
|
|
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/
|
|
2
|
-
-
|
|
3
|
-
-
|
|
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`;换产品/根意图/已终态/重新开始/重新走流程/作废本轮流程才新建。第三方 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
|
|
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
|
|