@xiashe/sdk 0.1.17 → 0.1.19

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 CHANGED
@@ -12,6 +12,110 @@ const SENSITIVE_USER_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_sig
12
12
  const DEFAULT_STATE_DIR = path.join(os.homedir(), '.xiashe', 'alipay-402');
13
13
  const OFFICIAL_MEDIA_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
14
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
+ // AI收 payment.verify business errors. These can be returned after the
22
+ // wallet has paid and retries the protected resource with Payment-Proof.
23
+ 'INVALID_PARAMETER',
24
+ 'CLIENT_SESSION_IS_EMPTY',
25
+ 'PAYMENT_PROOF_AGENT_MISMATCH',
26
+ 'PAYMENT_PROOF_BUYER_MISMATCH',
27
+ 'PAYMENT_PROOF_EMPTY',
28
+ 'PAYMENT_PROOF_ID_MISMATCH',
29
+ 'PAYMENT_PROOF_INVALID',
30
+ 'PAYMENT_PROOF_NOT_FOUND',
31
+ 'PAYMENT_PROOF_STATUS_INVALID',
32
+ 'PAYMENT_PROOF_TRADE_NO_MISMATCH',
33
+ 'TRADE_NOT_FOUND',
34
+ 'TRADE_NO_EMPTY',
35
+ 'TRADE_NO_INVALID',
36
+ 'TRADE_STATUS_CLOSED',
37
+ 'TRADE_STATUS_UNPAID',
38
+ // AI收 fulfillment.confirm business errors. These do not revoke a
39
+ // delivered entitlement, but do need an operator-visible diagnosis.
40
+ 'SELLER_IDENTITY_MISMATCH',
41
+ 'SELLER_ID_EMPTY',
42
+ 'TRADE_STATUS_INVALID'
43
+ ]);
44
+
45
+ const OFFICIAL_ERROR_DIAGNOSES = {
46
+ PAYMENT_NEEDED_REJECTED: {
47
+ stage: 'alipay_bill_validation',
48
+ nextStep: '核对支付宝平台中该 APPID 的应用公钥、service_id、收款方 2088 seller_id 和线上接入状态;修复后必须创建新订单再测试。'
49
+ },
50
+ CLIENT_SESSION_IS_EMPTY: {
51
+ stage: 'merchant_payment_proof_verification',
52
+ nextStep: '买方 CLI 未传回有效 client_session;升级官方支付宝支付组件后,用新订单重新发起付款。'
53
+ },
54
+ PAYMENT_PROOF_AGENT_MISMATCH: {
55
+ stage: 'merchant_payment_proof_verification',
56
+ nextStep: '支付凭证与当前 Agent 商户身份不一致;确认付款与重试使用同一官方钱包运行时后,以新订单重试。'
57
+ },
58
+ PAYMENT_PROOF_BUYER_MISMATCH: {
59
+ stage: 'merchant_payment_proof_verification',
60
+ nextStep: '支付买家与凭证买家不一致;不要复用凭证,请在同一支付宝买家身份下重新发起付款。'
61
+ },
62
+ PAYMENT_PROOF_ID_MISMATCH: {
63
+ stage: 'merchant_payment_proof_verification',
64
+ nextStep: 'Payment-Proof 中的卖家身份与本 Skill 配置不一致;核对 APPID、2088 seller_id 和 service_id。'
65
+ },
66
+ PAYMENT_PROOF_EMPTY: {
67
+ stage: 'merchant_payment_proof_verification',
68
+ nextStep: '官方钱包没有返回支付凭证;请停止,不要手工补造凭证,并用受支持运行时重新发起。'
69
+ },
70
+ PAYMENT_PROOF_INVALID: {
71
+ stage: 'merchant_payment_proof_verification',
72
+ nextStep: '支付宝返回的支付凭证格式无效;请使用最新官方买方组件,并创建新订单重试。'
73
+ },
74
+ PAYMENT_PROOF_NOT_FOUND: {
75
+ stage: 'merchant_payment_proof_verification',
76
+ nextStep: '支付凭证不存在或已过期;不要重复提交旧凭证,创建新订单后重新付款。'
77
+ },
78
+ PAYMENT_PROOF_STATUS_INVALID: {
79
+ stage: 'merchant_payment_proof_verification',
80
+ nextStep: '支付凭证已失效或已被使用;请创建新订单重新付款。'
81
+ },
82
+ PAYMENT_PROOF_TRADE_NO_MISMATCH: {
83
+ stage: 'merchant_payment_proof_verification',
84
+ nextStep: '支付凭证与交易号不匹配;不要重放该凭证,创建新订单后重新付款。'
85
+ },
86
+ TRADE_NOT_FOUND: {
87
+ stage: 'merchant_payment_proof_verification',
88
+ nextStep: '支付宝找不到该交易;先查询官方钱包本地状态,未创建交易时用新订单重新发起。'
89
+ },
90
+ TRADE_NO_EMPTY: {
91
+ stage: 'merchant_payment_proof_verification',
92
+ nextStep: '官方钱包未返回交易号;当前交易不可继续,请创建新订单重新发起。'
93
+ },
94
+ TRADE_NO_INVALID: {
95
+ stage: 'merchant_payment_proof_verification',
96
+ nextStep: '官方钱包返回的交易号格式无效;升级官方买方组件后以新订单重试。'
97
+ },
98
+ TRADE_STATUS_CLOSED: {
99
+ stage: 'merchant_payment_proof_verification',
100
+ nextStep: '该交易已关闭;不要重试旧订单,请创建新订单重新付款。'
101
+ },
102
+ TRADE_STATUS_UNPAID: {
103
+ stage: 'merchant_payment_proof_verification',
104
+ nextStep: '交易尚未完成付款;如本地有查询凭证则运行 status,否则创建新订单重新发起。'
105
+ },
106
+ SELLER_IDENTITY_MISMATCH: {
107
+ stage: 'merchant_fulfillment_confirmation',
108
+ nextStep: '已交付的 Skill 权益不会撤销;运营方应核对卖家 APPID、2088 seller_id 与交易卖方身份。'
109
+ },
110
+ SELLER_ID_EMPTY: {
111
+ stage: 'merchant_fulfillment_confirmation',
112
+ nextStep: '已交付的 Skill 权益不会撤销;运营方应检查商户应用签名与卖家身份配置。'
113
+ },
114
+ TRADE_STATUS_INVALID: {
115
+ stage: 'merchant_fulfillment_confirmation',
116
+ nextStep: '已交付的 Skill 权益不会撤销;运营方应查询支付宝交易状态后处理履约回执。'
117
+ }
118
+ };
15
119
 
16
120
  export class AlipayWalletError extends Error {
17
121
  constructor(code, message, options = {}) {
@@ -230,6 +334,113 @@ function walletFailureCode(stdout, stderr, exitCode) {
230
334
  return Number.isInteger(exitCode) && exitCode !== 0 ? 'official_cli_nonzero_exit' : 'official_cli_unclassified';
231
335
  }
232
336
 
337
+ /**
338
+ * The official wallet can return useful machine error codes alongside protocol
339
+ * material. Expose only a small allow-list: arbitrary vendor output can carry
340
+ * credentials, URLs, trade identifiers, or PayGuard internals.
341
+ */
342
+ function safeOfficialFailureCode(stdout, stderr) {
343
+ const output = `${stdout}\n${stderr}`;
344
+ const candidates = [
345
+ ...output.matchAll(/(?:"(?:errorCode|code|sub_code|previous_error)"|\b(?:errorCode|code|sub_code|previous_error)\b)\s*[:=]\s*["']?([A-Z][A-Z0-9_]{2,80})/ig)
346
+ ].map((match) => match[1].toUpperCase());
347
+ return candidates.find((candidate) => SAFE_OFFICIAL_FAILURE_CODES.has(candidate)) || null;
348
+ }
349
+
350
+ /** Codes generated by XiaShe from the signed merchant verification response. */
351
+ function safeMerchantDiagnosticCode(stdout, stderr) {
352
+ const output = `${stdout}\n${stderr}`;
353
+ const candidates = [...output.matchAll(/\b(ALIPAY_(?:VERIFY|FULFILLMENT)_[A-Z0-9_]{2,180})\b/g)]
354
+ .map((match) => match[1]);
355
+ return candidates.find((candidate) =>
356
+ /^ALIPAY_(?:VERIFY|FULFILLMENT)_(?:PAYMENT_PROOF_INACTIVE|(?:SYSTEM_ERROR|INVALID_PARAMETER|CLIENT_SESSION_IS_EMPTY|PAYMENT_PROOF_AGENT_MISMATCH|PAYMENT_PROOF_BUYER_MISMATCH|PAYMENT_PROOF_EMPTY|PAYMENT_PROOF_ID_MISMATCH|PAYMENT_PROOF_INVALID|PAYMENT_PROOF_NOT_FOUND|PAYMENT_PROOF_STATUS_INVALID|PAYMENT_PROOF_TRADE_NO_MISMATCH|SELLER_IDENTITY_MISMATCH|SELLER_ID_EMPTY|TRADE_NOT_FOUND|TRADE_NO_EMPTY|TRADE_NO_INVALID|TRADE_STATUS_CLOSED|TRADE_STATUS_UNPAID|TRADE_STATUS_INVALID)|GATEWAY_(?:20000|20001|40001|40002|40003|40004|40005|40006)(?:_[A-Z0-9_]{2,100})?)$/.test(candidate)
357
+ ) || null;
358
+ }
359
+
360
+ function paymentFailureDiagnosis(failureCode, stdout, stderr) {
361
+ const officialCode = safeOfficialFailureCode(stdout, stderr);
362
+ const merchantDiagnostic = safeMerchantDiagnosticCode(stdout, stderr);
363
+ const base = {
364
+ code: failureCode,
365
+ stage: 'official_wallet_trade_creation',
366
+ nextStep: '不要重复付款;保留诊断代码和本次 attemptId 以便定位。'
367
+ };
368
+ if (officialCode && OFFICIAL_ERROR_DIAGNOSES[officialCode]) {
369
+ return {
370
+ ...base,
371
+ ...OFFICIAL_ERROR_DIAGNOSES[officialCode],
372
+ officialCode
373
+ };
374
+ }
375
+ if (merchantDiagnostic) {
376
+ const fulfillment = merchantDiagnostic.startsWith('ALIPAY_FULFILLMENT_');
377
+ return {
378
+ ...base,
379
+ stage: fulfillment ? 'merchant_fulfillment_confirmation' : 'merchant_payment_proof_verification',
380
+ merchantDiagnostic,
381
+ nextStep: fulfillment
382
+ ? 'Skill 权益已交付;运营方应按商家验证诊断处理履约回执,买家无需重复付款。'
383
+ : '商家已返回支付宝验凭证诊断;不要手工补造凭证,按诊断修复后创建新订单重新付款。'
384
+ };
385
+ }
386
+ if (failureCode === 'payment_needed_rejected') {
387
+ return {
388
+ ...base,
389
+ stage: 'alipay_bill_validation',
390
+ nextStep: '核对支付宝平台中该 APPID 的应用公钥、service_id、收款方 2088 seller_id 和线上接入状态;修复后必须创建新订单再测试。',
391
+ ...(officialCode ? { officialCode } : {})
392
+ };
393
+ }
394
+ if (failureCode === 'wallet_runtime_not_supported') {
395
+ return {
396
+ ...base,
397
+ stage: 'agent_runtime_identity',
398
+ nextStep: '当前运行时没有通过支付宝 Agent 身份校验;请换用已被官方钱包识别并已授权的本地 Agent 运行时。',
399
+ ...(officialCode ? { officialCode } : {})
400
+ };
401
+ }
402
+ if (failureCode === 'buyer_identity_rejected') {
403
+ return {
404
+ ...base,
405
+ stage: 'buyer_identity_validation',
406
+ nextStep: '支付宝钱包的买家身份未通过校验;完成钱包授权后,用同一受支持运行时创建新订单。',
407
+ ...(officialCode ? { officialCode } : {})
408
+ };
409
+ }
410
+ if (failureCode === 'wallet_not_ready') {
411
+ return {
412
+ ...base,
413
+ stage: 'wallet_authorization',
414
+ nextStep: '支付宝钱包未完成授权或未启用;先完成官方钱包授权,再创建新订单。',
415
+ ...(officialCode ? { officialCode } : {})
416
+ };
417
+ }
418
+ if (failureCode === 'wallet_network_error') {
419
+ return {
420
+ ...base,
421
+ stage: 'wallet_network',
422
+ nextStep: '钱包或网络请求没有完成;先检查网络,再创建新订单。',
423
+ ...(officialCode ? { officialCode } : {})
424
+ };
425
+ }
426
+ return {
427
+ ...base,
428
+ ...(officialCode ? { officialCode } : {})
429
+ };
430
+ }
431
+
432
+ function paymentFailureUserVisible(diagnosis) {
433
+ const lines = [
434
+ '支付宝付款未创建可确认交易。',
435
+ `诊断代码:${diagnosis.code}`,
436
+ `失败阶段:${diagnosis.stage}`,
437
+ ...(diagnosis.officialCode ? [`官方返回代码:${diagnosis.officialCode}`] : []),
438
+ ...(diagnosis.merchantDiagnostic ? [`商家验证诊断:${diagnosis.merchantDiagnostic}`] : []),
439
+ `下一步:${diagnosis.nextStep}`
440
+ ];
441
+ return lines.join('\n');
442
+ }
443
+
233
444
  function paymentMessage(status) {
234
445
  if (status === 'completed') return '支付宝钱包显示本次付款已完成;正在由服务端确认权益。';
235
446
  if (status === 'pending_confirmation') return '支付宝已发起本次付款确认;请打开支付宝 App 完成确认,然后运行 status。';
@@ -285,6 +496,7 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
285
496
  const commandFailed = options.commandFailed === true;
286
497
  const failureCode = options.failureCode || 'official_cli_nonzero_exit';
287
498
  const failed = status === 'failed' || status === 'wallet_command_failed';
499
+ const diagnosis = failed ? options.diagnosis : null;
288
500
  return {
289
501
  ok: !failed,
290
502
  status,
@@ -293,7 +505,8 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
293
505
  walletInvoked: true,
294
506
  message: failed ? paymentFailureMessage(failureCode) : paymentMessage(status),
295
507
  diagnostic,
296
- ...(options.userVisible ? { userVisible: options.userVisible } : {}),
508
+ ...(failed && diagnosis ? { userVisible: paymentFailureUserVisible(diagnosis), diagnosis } : {}),
509
+ ...(!failed && options.userVisible ? { userVisible: options.userVisible } : {}),
297
510
  ...(options.officialMedia ? { officialMedia: options.officialMedia } : {}),
298
511
  ...(status === 'pending_confirmation'
299
512
  ? { confirmation: { source: 'official_alipay_wallet', surface: 'app_or_official_media', required: true } }
@@ -383,18 +596,25 @@ export async function payAlipay402Resource(input, options = {}) {
383
596
  const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
384
597
  const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
385
598
  const status = commandFailed ? 'wallet_command_failed' : result.status;
599
+ const failed = commandFailed || status === 'failed';
386
600
  const officialMedia = ['pending_confirmation', 'completed'].includes(status)
387
601
  ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
388
602
  : null;
389
603
  state.status = status;
390
604
  state.queryCredential = result.credential;
605
+ state.lastFailureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : null;
606
+ state.lastDiagnosis = failed
607
+ ? paymentFailureDiagnosis(state.lastFailureCode, walletInvocation.stdout, walletInvocation.stderr)
608
+ : null;
609
+ state.lastWalletDiagnostic = result.diagnostic;
391
610
  state.updatedAt = new Date().toISOString();
392
611
  await writePaymentState(state, options);
393
612
  if (status === 'completed') await rm(paths.paymentNeededPath, { force: true });
394
- const failed = commandFailed || status === 'failed';
613
+ const failureCode = failed ? state.lastFailureCode : undefined;
395
614
  return publicPaymentResult(state, status, result.diagnostic, {
396
615
  commandFailed: failed,
397
- failureCode: failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined,
616
+ failureCode,
617
+ diagnosis: failed ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr) : undefined,
398
618
  userVisible: ['pending_confirmation', 'completed'].includes(status)
399
619
  ? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
400
620
  : null,
@@ -428,24 +648,64 @@ export async function queryAlipay402Resource(input, options = {}) {
428
648
  const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
429
649
  const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
430
650
  const status = commandFailed ? 'wallet_command_failed' : result.status;
651
+ const failed = commandFailed || status === 'failed';
431
652
  const officialMedia = ['pending_confirmation', 'completed'].includes(status)
432
653
  ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
433
654
  : null;
434
655
  state.status = status;
435
656
  state.queryCredential = result.credential || state.queryCredential;
657
+ state.lastFailureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : null;
658
+ state.lastDiagnosis = failed
659
+ ? paymentFailureDiagnosis(state.lastFailureCode, walletInvocation.stdout, walletInvocation.stderr)
660
+ : null;
661
+ state.lastWalletDiagnostic = result.diagnostic;
436
662
  state.updatedAt = new Date().toISOString();
437
663
  await writePaymentState(state, options);
438
664
  if (status === 'completed') {
439
665
  const { paymentNeededPath } = statePaths(resourceUrl, options);
440
666
  await rm(paymentNeededPath, { force: true });
441
667
  }
442
- const failed = commandFailed || status === 'failed';
668
+ const failureCode = failed ? state.lastFailureCode : undefined;
443
669
  return publicPaymentResult(state, status, result.diagnostic, {
444
670
  commandFailed: failed,
445
- failureCode: failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined,
671
+ failureCode,
672
+ diagnosis: failed ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr) : undefined,
446
673
  userVisible: ['pending_confirmation', 'completed'].includes(status)
447
674
  ? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
448
675
  : null,
449
676
  officialMedia
450
677
  });
451
678
  }
679
+
680
+ /**
681
+ * Read-only support output. It never invokes the wallet, replays a 402
682
+ * request, exposes a query credential, or returns the private protocol file.
683
+ */
684
+ export async function diagnoseAlipay402Resource(input, options = {}) {
685
+ const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
686
+ const state = await readPaymentState(resourceUrl, options);
687
+ if (!state) {
688
+ return {
689
+ ok: false,
690
+ status: 'no_local_attempt',
691
+ provider: 'alipay-agent-payment',
692
+ walletInvoked: false,
693
+ message: '没有找到本地付款尝试;请不要因此重复付款。'
694
+ };
695
+ }
696
+ return {
697
+ ok: state.status !== 'failed' && state.status !== 'wallet_command_failed',
698
+ status: state.status || 'unknown',
699
+ provider: 'alipay-agent-payment',
700
+ attemptId: `alipay-402:${state.resourceDigest.slice(0, 16)}`,
701
+ walletInvoked: state.status !== 'started',
702
+ queryAvailable: Boolean(state.queryCredential?.value),
703
+ diagnostic: state.lastWalletDiagnostic || 'payment_state_recorded',
704
+ ...(state.lastFailureCode ? { failureCode: state.lastFailureCode } : {}),
705
+ ...(state.lastDiagnosis ? {
706
+ diagnosis: state.lastDiagnosis,
707
+ userVisible: paymentFailureUserVisible(state.lastDiagnosis)
708
+ } : {}),
709
+ updatedAt: state.updatedAt || null
710
+ };
711
+ }
package/index.mjs CHANGED
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  import { ConvexHttpClient } from 'convex/browser';
7
7
  import { anyApi as api } from 'convex/server';
8
8
 
9
- export const VERSION = '0.1.17';
9
+ export const VERSION = '0.1.19';
10
10
 
11
11
  export const DEFAULTS = {
12
12
  cn: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiashe/sdk",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {