@xiashe/sdk 0.1.18 → 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
@@ -17,9 +17,106 @@ const SAFE_OFFICIAL_FAILURE_CODES = new Set([
17
17
  'PAYMENT_NEEDED_REJECTED',
18
18
  'SYSTEM_ERROR',
19
19
  'BUYER_IDENTITY_REJECTED',
20
- 'WALLET_NOT_READY'
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'
21
43
  ]);
22
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
+ };
119
+
23
120
  export class AlipayWalletError extends Error {
24
121
  constructor(code, message, options = {}) {
25
122
  super(message);
@@ -244,17 +341,48 @@ function walletFailureCode(stdout, stderr, exitCode) {
244
341
  */
245
342
  function safeOfficialFailureCode(stdout, stderr) {
246
343
  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;
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;
249
358
  }
250
359
 
251
360
  function paymentFailureDiagnosis(failureCode, stdout, stderr) {
252
361
  const officialCode = safeOfficialFailureCode(stdout, stderr);
362
+ const merchantDiagnostic = safeMerchantDiagnosticCode(stdout, stderr);
253
363
  const base = {
254
364
  code: failureCode,
255
365
  stage: 'official_wallet_trade_creation',
256
366
  nextStep: '不要重复付款;保留诊断代码和本次 attemptId 以便定位。'
257
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
+ }
258
386
  if (failureCode === 'payment_needed_rejected') {
259
387
  return {
260
388
  ...base,
@@ -307,6 +435,7 @@ function paymentFailureUserVisible(diagnosis) {
307
435
  `诊断代码:${diagnosis.code}`,
308
436
  `失败阶段:${diagnosis.stage}`,
309
437
  ...(diagnosis.officialCode ? [`官方返回代码:${diagnosis.officialCode}`] : []),
438
+ ...(diagnosis.merchantDiagnostic ? [`商家验证诊断:${diagnosis.merchantDiagnostic}`] : []),
310
439
  `下一步:${diagnosis.nextStep}`
311
440
  ];
312
441
  return lines.join('\n');
@@ -467,16 +596,21 @@ export async function payAlipay402Resource(input, options = {}) {
467
596
  const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
468
597
  const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
469
598
  const status = commandFailed ? 'wallet_command_failed' : result.status;
599
+ const failed = commandFailed || status === 'failed';
470
600
  const officialMedia = ['pending_confirmation', 'completed'].includes(status)
471
601
  ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
472
602
  : null;
473
603
  state.status = status;
474
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;
475
610
  state.updatedAt = new Date().toISOString();
476
611
  await writePaymentState(state, options);
477
612
  if (status === 'completed') await rm(paths.paymentNeededPath, { force: true });
478
- const failed = commandFailed || status === 'failed';
479
- const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
613
+ const failureCode = failed ? state.lastFailureCode : undefined;
480
614
  return publicPaymentResult(state, status, result.diagnostic, {
481
615
  commandFailed: failed,
482
616
  failureCode,
@@ -514,19 +648,24 @@ export async function queryAlipay402Resource(input, options = {}) {
514
648
  const result = paymentStatusFromOutput(walletInvocation.stdout, walletInvocation.stderr);
515
649
  const commandFailed = walletInvocation.exitCode !== 0 && result.status === 'unknown';
516
650
  const status = commandFailed ? 'wallet_command_failed' : result.status;
651
+ const failed = commandFailed || status === 'failed';
517
652
  const officialMedia = ['pending_confirmation', 'completed'].includes(status)
518
653
  ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
519
654
  : null;
520
655
  state.status = status;
521
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;
522
662
  state.updatedAt = new Date().toISOString();
523
663
  await writePaymentState(state, options);
524
664
  if (status === 'completed') {
525
665
  const { paymentNeededPath } = statePaths(resourceUrl, options);
526
666
  await rm(paymentNeededPath, { force: true });
527
667
  }
528
- const failed = commandFailed || status === 'failed';
529
- const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
668
+ const failureCode = failed ? state.lastFailureCode : undefined;
530
669
  return publicPaymentResult(state, status, result.diagnostic, {
531
670
  commandFailed: failed,
532
671
  failureCode,
@@ -537,3 +676,36 @@ export async function queryAlipay402Resource(input, options = {}) {
537
676
  officialMedia
538
677
  });
539
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.18';
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.18",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {