@xiashe/sdk 0.1.16 → 0.1.18
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/alipayWallet.mjs +139 -8
- package/index.mjs +1 -1
- package/package.json +1 -1
package/alipayWallet.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
|
-
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { chmod, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
|
|
@@ -10,6 +10,15 @@ const MAX_OUTPUT_BYTES = 32 * 1024;
|
|
|
10
10
|
const SENSITIVE_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_signature|runtimeToken|authorization\s*:/i;
|
|
11
11
|
const SENSITIVE_USER_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_signature|runtimeToken|authorization\s*:|trade[_ -]?no|交易(?:流水)?号|out[_ -]?shake|client[_ -]?session|payguard|validateclientparent|agentfamily/i;
|
|
12
12
|
const DEFAULT_STATE_DIR = path.join(os.homedir(), '.xiashe', 'alipay-402');
|
|
13
|
+
const OFFICIAL_MEDIA_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
|
|
14
|
+
const MAX_OFFICIAL_MEDIA_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
const SAFE_OFFICIAL_FAILURE_CODES = new Set([
|
|
16
|
+
'TRADE_PROGRESS_FAILED',
|
|
17
|
+
'PAYMENT_NEEDED_REJECTED',
|
|
18
|
+
'SYSTEM_ERROR',
|
|
19
|
+
'BUYER_IDENTITY_REJECTED',
|
|
20
|
+
'WALLET_NOT_READY'
|
|
21
|
+
]);
|
|
13
22
|
|
|
14
23
|
export class AlipayWalletError extends Error {
|
|
15
24
|
constructor(code, message, options = {}) {
|
|
@@ -176,7 +185,11 @@ function paymentStatusFromOutput(stdout, stderr) {
|
|
|
176
185
|
const output = `${stdout}\n${stderr}`;
|
|
177
186
|
const completed = /支付(?:已)?成功|交易成功|支付已完成|payment\s+(?:was\s+)?(?:successful|completed)|(?:resource|资源).*(?:obtained|success|成功)/i.test(output);
|
|
178
187
|
const pending = /支付待确认|等待.*确认|待确认|支付处理中|pending(?:\s+confirmation)?|waiting.*confirm/i.test(output);
|
|
179
|
-
|
|
188
|
+
// The official CLI sometimes returns a JSON error object with exit code 0.
|
|
189
|
+
// Treat an explicit unsuccessful result as failed so callers retain the
|
|
190
|
+
// normalized failure category instead of misleadingly reporting an unknown
|
|
191
|
+
// wallet state.
|
|
192
|
+
const failed = /支付失败|交易失败|payment\s+failed|"success"\s*:\s*false|"errorCode"\s*:/i.test(output);
|
|
180
193
|
return {
|
|
181
194
|
status: completed ? 'completed' : pending ? 'pending_confirmation' : failed ? 'failed' : 'unknown',
|
|
182
195
|
credential: queryCredentialFromOutput(output),
|
|
@@ -189,14 +202,31 @@ function userVisibleWalletOutput(stdout, stderr) {
|
|
|
189
202
|
.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '')
|
|
190
203
|
.split(/\r?\n/)
|
|
191
204
|
.map((line) => line.trim())
|
|
192
|
-
.filter((line) => line && !SENSITIVE_USER_OUTPUT.test(line))
|
|
205
|
+
.filter((line) => line && !line.startsWith('MEDIA:') && !SENSITIVE_USER_OUTPUT.test(line))
|
|
193
206
|
.slice(0, 12);
|
|
194
207
|
return lines.length ? lines.join('\n').slice(0, 1_200) : null;
|
|
195
208
|
}
|
|
196
209
|
|
|
210
|
+
/** Preserve only an image emitted by the official wallet in this invocation. */
|
|
211
|
+
async function officialWalletMedia(stdout, stderr) {
|
|
212
|
+
const mediaLine = `${stdout}\n${stderr}`.split(/\r?\n/).find((line) => line.trim().startsWith('MEDIA:'));
|
|
213
|
+
if (!mediaLine) return null;
|
|
214
|
+
const candidate = mediaLine.slice(mediaLine.indexOf('MEDIA:') + 'MEDIA:'.length).trim();
|
|
215
|
+
if (!candidate || !path.isAbsolute(candidate) || !OFFICIAL_MEDIA_EXTENSIONS.has(path.extname(candidate).toLowerCase())) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const file = await stat(candidate);
|
|
220
|
+
return file.isFile() && file.size > 0 && file.size <= MAX_OFFICIAL_MEDIA_BYTES ? candidate : null;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
197
226
|
function walletFailureCode(stdout, stderr, exitCode) {
|
|
198
227
|
const output = `${stdout}\n${stderr}`;
|
|
199
228
|
if (/TRADE_PROGRESS_FAILED/i.test(output)) return 'wallet_trade_progress_failed';
|
|
229
|
+
if (/payment[_ -]?needed[_ -]?rejected|账单.*(?:被拒绝|拒绝)|bill.*(?:rejected|denied)/i.test(output)) return 'payment_needed_rejected';
|
|
200
230
|
if (/余额不足|insufficient\s+balance/i.test(output)) return 'insufficient_balance';
|
|
201
231
|
if (/已过期|过期|expired/i.test(output)) return 'payment_expired';
|
|
202
232
|
if (/钱包.*(?:未开启|未授权)|wallet.*(?:not\s+enabled|not\s+authorized)/i.test(output)) return 'wallet_not_ready';
|
|
@@ -207,6 +237,81 @@ function walletFailureCode(stdout, stderr, exitCode) {
|
|
|
207
237
|
return Number.isInteger(exitCode) && exitCode !== 0 ? 'official_cli_nonzero_exit' : 'official_cli_unclassified';
|
|
208
238
|
}
|
|
209
239
|
|
|
240
|
+
/**
|
|
241
|
+
* The official wallet can return useful machine error codes alongside protocol
|
|
242
|
+
* material. Expose only a small allow-list: arbitrary vendor output can carry
|
|
243
|
+
* credentials, URLs, trade identifiers, or PayGuard internals.
|
|
244
|
+
*/
|
|
245
|
+
function safeOfficialFailureCode(stdout, stderr) {
|
|
246
|
+
const output = `${stdout}\n${stderr}`;
|
|
247
|
+
const candidate = output.match(/(?:"(?:errorCode|code)"|\b(?:errorCode|code)\b)\s*[:=]\s*["']?([A-Z][A-Z0-9_]{2,80})/i)?.[1]?.toUpperCase() || '';
|
|
248
|
+
return SAFE_OFFICIAL_FAILURE_CODES.has(candidate) ? candidate : null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function paymentFailureDiagnosis(failureCode, stdout, stderr) {
|
|
252
|
+
const officialCode = safeOfficialFailureCode(stdout, stderr);
|
|
253
|
+
const base = {
|
|
254
|
+
code: failureCode,
|
|
255
|
+
stage: 'official_wallet_trade_creation',
|
|
256
|
+
nextStep: '不要重复付款;保留诊断代码和本次 attemptId 以便定位。'
|
|
257
|
+
};
|
|
258
|
+
if (failureCode === 'payment_needed_rejected') {
|
|
259
|
+
return {
|
|
260
|
+
...base,
|
|
261
|
+
stage: 'alipay_bill_validation',
|
|
262
|
+
nextStep: '核对支付宝平台中该 APPID 的应用公钥、service_id、收款方 2088 seller_id 和线上接入状态;修复后必须创建新订单再测试。',
|
|
263
|
+
...(officialCode ? { officialCode } : {})
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (failureCode === 'wallet_runtime_not_supported') {
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
stage: 'agent_runtime_identity',
|
|
270
|
+
nextStep: '当前运行时没有通过支付宝 Agent 身份校验;请换用已被官方钱包识别并已授权的本地 Agent 运行时。',
|
|
271
|
+
...(officialCode ? { officialCode } : {})
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (failureCode === 'buyer_identity_rejected') {
|
|
275
|
+
return {
|
|
276
|
+
...base,
|
|
277
|
+
stage: 'buyer_identity_validation',
|
|
278
|
+
nextStep: '支付宝钱包的买家身份未通过校验;完成钱包授权后,用同一受支持运行时创建新订单。',
|
|
279
|
+
...(officialCode ? { officialCode } : {})
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (failureCode === 'wallet_not_ready') {
|
|
283
|
+
return {
|
|
284
|
+
...base,
|
|
285
|
+
stage: 'wallet_authorization',
|
|
286
|
+
nextStep: '支付宝钱包未完成授权或未启用;先完成官方钱包授权,再创建新订单。',
|
|
287
|
+
...(officialCode ? { officialCode } : {})
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
if (failureCode === 'wallet_network_error') {
|
|
291
|
+
return {
|
|
292
|
+
...base,
|
|
293
|
+
stage: 'wallet_network',
|
|
294
|
+
nextStep: '钱包或网络请求没有完成;先检查网络,再创建新订单。',
|
|
295
|
+
...(officialCode ? { officialCode } : {})
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
...base,
|
|
300
|
+
...(officialCode ? { officialCode } : {})
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function paymentFailureUserVisible(diagnosis) {
|
|
305
|
+
const lines = [
|
|
306
|
+
'支付宝付款未创建可确认交易。',
|
|
307
|
+
`诊断代码:${diagnosis.code}`,
|
|
308
|
+
`失败阶段:${diagnosis.stage}`,
|
|
309
|
+
...(diagnosis.officialCode ? [`官方返回代码:${diagnosis.officialCode}`] : []),
|
|
310
|
+
`下一步:${diagnosis.nextStep}`
|
|
311
|
+
];
|
|
312
|
+
return lines.join('\n');
|
|
313
|
+
}
|
|
314
|
+
|
|
210
315
|
function paymentMessage(status) {
|
|
211
316
|
if (status === 'completed') return '支付宝钱包显示本次付款已完成;正在由服务端确认权益。';
|
|
212
317
|
if (status === 'pending_confirmation') return '支付宝已发起本次付款确认;请打开支付宝 App 完成确认,然后运行 status。';
|
|
@@ -221,6 +326,9 @@ function paymentFailureMessage(failureCode) {
|
|
|
221
326
|
if (failureCode === 'wallet_runtime_not_supported') {
|
|
222
327
|
return '当前 Agent 运行环境不支持支付宝 AI付;未授予付费 Skill 权益。';
|
|
223
328
|
}
|
|
329
|
+
if (failureCode === 'payment_needed_rejected') {
|
|
330
|
+
return '支付宝端拒绝了本次付款账单,未创建交易、未扣款。请确认该 APPID 已在支付宝完成 AI按量付费的服务注册、密钥配置、技术集成确认与线上验证前置步骤;若这些均已完成,请提交支付宝问题反馈并附上本次发生时间。';
|
|
331
|
+
}
|
|
224
332
|
return '支付宝官方钱包没有生成可确认的付款状态;未授予付费 Skill 权益。';
|
|
225
333
|
}
|
|
226
334
|
|
|
@@ -259,6 +367,7 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
|
|
|
259
367
|
const commandFailed = options.commandFailed === true;
|
|
260
368
|
const failureCode = options.failureCode || 'official_cli_nonzero_exit';
|
|
261
369
|
const failed = status === 'failed' || status === 'wallet_command_failed';
|
|
370
|
+
const diagnosis = failed ? options.diagnosis : null;
|
|
262
371
|
return {
|
|
263
372
|
ok: !failed,
|
|
264
373
|
status,
|
|
@@ -267,7 +376,12 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
|
|
|
267
376
|
walletInvoked: true,
|
|
268
377
|
message: failed ? paymentFailureMessage(failureCode) : paymentMessage(status),
|
|
269
378
|
diagnostic,
|
|
270
|
-
...(
|
|
379
|
+
...(failed && diagnosis ? { userVisible: paymentFailureUserVisible(diagnosis), diagnosis } : {}),
|
|
380
|
+
...(!failed && options.userVisible ? { userVisible: options.userVisible } : {}),
|
|
381
|
+
...(options.officialMedia ? { officialMedia: options.officialMedia } : {}),
|
|
382
|
+
...(status === 'pending_confirmation'
|
|
383
|
+
? { confirmation: { source: 'official_alipay_wallet', surface: 'app_or_official_media', required: true } }
|
|
384
|
+
: { confirmation: { source: 'official_alipay_wallet', required: false } }),
|
|
271
385
|
...(commandFailed ? { failureCode } : {})
|
|
272
386
|
};
|
|
273
387
|
}
|
|
@@ -341,6 +455,10 @@ export async function payAlipay402Resource(input, options = {}) {
|
|
|
341
455
|
|
|
342
456
|
const walletInvocation = await runOfficialCli([
|
|
343
457
|
'402-buyer-pay',
|
|
458
|
+
// The official buyer CLI accepts this long form of -s/-f/-r. The file
|
|
459
|
+
// name is intentionally private and random; the exact Payment-Needed
|
|
460
|
+
// content, original resource URL, and generated UUID session are what
|
|
461
|
+
// define the payment request.
|
|
344
462
|
'--session-id', state.sessionId,
|
|
345
463
|
'--file', state.paymentNeededPath,
|
|
346
464
|
'--resource-url', resourceUrl,
|
|
@@ -349,18 +467,24 @@ export async function payAlipay402Resource(input, options = {}) {
|
|
|
349
467
|
const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
|
|
350
468
|
const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
|
|
351
469
|
const status = commandFailed ? 'wallet_command_failed' : result.status;
|
|
470
|
+
const officialMedia = ['pending_confirmation', 'completed'].includes(status)
|
|
471
|
+
? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
|
|
472
|
+
: null;
|
|
352
473
|
state.status = status;
|
|
353
474
|
state.queryCredential = result.credential;
|
|
354
475
|
state.updatedAt = new Date().toISOString();
|
|
355
476
|
await writePaymentState(state, options);
|
|
356
477
|
if (status === 'completed') await rm(paths.paymentNeededPath, { force: true });
|
|
357
478
|
const failed = commandFailed || status === 'failed';
|
|
479
|
+
const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
|
|
358
480
|
return publicPaymentResult(state, status, result.diagnostic, {
|
|
359
481
|
commandFailed: failed,
|
|
360
|
-
failureCode
|
|
482
|
+
failureCode,
|
|
483
|
+
diagnosis: failed ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr) : undefined,
|
|
361
484
|
userVisible: ['pending_confirmation', 'completed'].includes(status)
|
|
362
485
|
? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
|
|
363
|
-
: null
|
|
486
|
+
: null,
|
|
487
|
+
officialMedia
|
|
364
488
|
});
|
|
365
489
|
}
|
|
366
490
|
|
|
@@ -374,6 +498,7 @@ export async function queryAlipay402Resource(input, options = {}) {
|
|
|
374
498
|
status: 'no_active_payment',
|
|
375
499
|
provider: 'alipay-agent-payment',
|
|
376
500
|
walletInvoked: false,
|
|
501
|
+
confirmation: { source: 'official_alipay_wallet', required: false },
|
|
377
502
|
message: '没有可查询的本地支付宝付款记录;请重新发起付款,不要把付款凭证粘贴到对话中。',
|
|
378
503
|
diagnostic: 'payment_state_not_found'
|
|
379
504
|
};
|
|
@@ -389,6 +514,9 @@ export async function queryAlipay402Resource(input, options = {}) {
|
|
|
389
514
|
const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
|
|
390
515
|
const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
|
|
391
516
|
const status = commandFailed ? 'wallet_command_failed' : result.status;
|
|
517
|
+
const officialMedia = ['pending_confirmation', 'completed'].includes(status)
|
|
518
|
+
? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
|
|
519
|
+
: null;
|
|
392
520
|
state.status = status;
|
|
393
521
|
state.queryCredential = result.credential || state.queryCredential;
|
|
394
522
|
state.updatedAt = new Date().toISOString();
|
|
@@ -398,11 +526,14 @@ export async function queryAlipay402Resource(input, options = {}) {
|
|
|
398
526
|
await rm(paymentNeededPath, { force: true });
|
|
399
527
|
}
|
|
400
528
|
const failed = commandFailed || status === 'failed';
|
|
529
|
+
const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
|
|
401
530
|
return publicPaymentResult(state, status, result.diagnostic, {
|
|
402
531
|
commandFailed: failed,
|
|
403
|
-
failureCode
|
|
532
|
+
failureCode,
|
|
533
|
+
diagnosis: failed ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr) : undefined,
|
|
404
534
|
userVisible: ['pending_confirmation', 'completed'].includes(status)
|
|
405
535
|
? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
|
|
406
|
-
: null
|
|
536
|
+
: null,
|
|
537
|
+
officialMedia
|
|
407
538
|
});
|
|
408
539
|
}
|
package/index.mjs
CHANGED