@xiashe/sdk 0.1.28 → 0.1.30

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,7 @@ 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 OUT_SHAKE_NO_PATTERN = /^[A-Za-z0-9_-]{32}$/;
15
16
  const SAFE_OFFICIAL_FAILURE_CODES = new Set([
16
17
  'TRADE_PROGRESS_FAILED',
17
18
  'PAYMENT_NEEDED_REJECTED',
@@ -203,6 +204,20 @@ function normalizeResourceUrl(value) {
203
204
  return url.toString();
204
205
  }
205
206
 
207
+ /**
208
+ * Alipay support may call this a "business order number", but the 402 status
209
+ * API accepts it only when it has the 32-character outShakeNo shape accepted
210
+ * by the official client. This is a query credential, not a merchant out_trade_no and never a
211
+ * Payment-Proof or fulfillment acknowledgement credential.
212
+ */
213
+ function normalizeOutShakeNo(value) {
214
+ const outShakeNo = String(value || '').trim();
215
+ if (!OUT_SHAKE_NO_PATTERN.test(outShakeNo)) {
216
+ throw new AlipayWalletError('ALIPAY_OUT_SHAKE_NO_INVALID', '业务单号不是有效的支付宝 AI付查询凭证。');
217
+ }
218
+ return outShakeNo;
219
+ }
220
+
206
221
  function normalizeIntentSummary(value) {
207
222
  const summary = safeText(value, 180);
208
223
  if (!summary) {
@@ -327,7 +342,7 @@ function queryCredentialFromOutput(output) {
327
342
  const outShakeNo = output.match(/(?:["']?out[\s_-]*shake[\s_-]*no["']?|外部(?:摇一摇)?编号)\s*["::=]\s*["']?([A-Za-z0-9_-]{8,128})/i)?.[1] || null;
328
343
  // An outShakeNo is the preferred 402 status-query credential, but only its
329
344
  // documented 32-character AI Pay form may select that query path.
330
- const validOutShakeNo = outShakeNo && outShakeNo.length === 32 && outShakeNo.slice(10, 14) === '8282';
345
+ const validOutShakeNo = outShakeNo && OUT_SHAKE_NO_PATTERN.test(outShakeNo);
331
346
  return validOutShakeNo
332
347
  ? { type: 'out_shake_no', value: outShakeNo }
333
348
  : tradeNo
@@ -343,9 +358,13 @@ function paymentStatusFromOutput(stdout, stderr) {
343
358
  // Treat an explicit unsuccessful result as failed so callers retain the
344
359
  // normalized failure category instead of misleadingly reporting an unknown
345
360
  // wallet state.
346
- const failed = /支付失败|交易失败|payment\s+failed|"success"\s*:\s*false|"errorCode"\s*:/i.test(output);
361
+ // A successful status-query transport can still include the protected
362
+ // resource's safe ALIPAY_VERIFY_* response. That is a merchant-side proof
363
+ // verification failure, not an unknown wallet state.
364
+ const merchantDiagnostic = safeMerchantDiagnosticCode(stdout, stderr);
365
+ const failed = Boolean(merchantDiagnostic) || /支付失败|交易失败|payment\s+failed|"success"\s*:\s*false|"errorCode"\s*:/i.test(output);
347
366
  return {
348
- status: completed ? 'completed' : pending ? 'pending_confirmation' : failed ? 'failed' : 'unknown',
367
+ status: failed ? 'failed' : completed ? 'completed' : pending ? 'pending_confirmation' : 'unknown',
349
368
  credential: queryCredentialFromOutput(output),
350
369
  diagnostic: safeDiagnostic(output)
351
370
  };
@@ -379,6 +398,7 @@ async function officialWalletMedia(stdout, stderr) {
379
398
 
380
399
  function walletFailureCode(stdout, stderr, exitCode) {
381
400
  const output = `${stdout}\n${stderr}`;
401
+ if (safeMerchantDiagnosticCode(stdout, stderr)) return 'merchant_payment_proof_verification_failed';
382
402
  if (/TRADE_PROGRESS_FAILED/i.test(output)) return 'wallet_trade_progress_failed';
383
403
  if (/payment[_ -]?needed[_ -]?rejected|账单.*(?:被拒绝|拒绝)|bill.*(?:rejected|denied)/i.test(output)) return 'payment_needed_rejected';
384
404
  if (/余额不足|insufficient\s+balance/i.test(output)) return 'insufficient_balance';
@@ -501,7 +521,9 @@ function paymentFailureDiagnosis(failureCode, stdout, stderr) {
501
521
 
502
522
  function paymentFailureUserVisible(diagnosis) {
503
523
  const lines = [
504
- diagnosis.code === 'wallet_query_credential_missing'
524
+ diagnosis.stage === 'merchant_payment_proof_verification'
525
+ ? '商家未能验证本次支付宝付款凭证,尚未授予付费 Skill 权益。'
526
+ : diagnosis.code === 'wallet_query_credential_missing'
505
527
  ? '支付宝钱包未返回可继续查询的交易凭据,当前付款状态无法确认。'
506
528
  : '支付宝付款未创建可确认交易。',
507
529
  `诊断代码:${diagnosis.code}`,
@@ -525,6 +547,9 @@ function paymentMessage(status) {
525
547
  }
526
548
 
527
549
  function paymentFailureMessage(failureCode) {
550
+ if (failureCode === 'merchant_payment_proof_verification_failed') {
551
+ return '商家未能验证支付宝付款凭证,未授予付费 Skill 权益;请勿重放旧凭证或重复付款。';
552
+ }
528
553
  if (failureCode === 'wallet_query_credential_missing') {
529
554
  return '支付宝钱包返回待确认,但未返回可继续查询的交易凭据。无法确认是否已付款,因此不会授予付费 Skill 权益;请勿再次付款。';
530
555
  }
@@ -684,10 +709,11 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
684
709
  ...(failed && diagnosis ? { userVisible: paymentFailureUserVisible(diagnosis), diagnosis } : {}),
685
710
  ...(!failed && options.userVisible ? { userVisible: options.userVisible } : {}),
686
711
  ...(options.officialMedia ? { officialMedia: options.officialMedia } : {}),
712
+ ...(options.queryCredentialSource ? { queryCredentialSource: options.queryCredentialSource } : {}),
687
713
  ...(status === 'pending_confirmation'
688
714
  ? { confirmation: { source: 'official_alipay_wallet', surface: 'app_or_official_media', required: true } }
689
715
  : { confirmation: { source: 'official_alipay_wallet', required: false } }),
690
- ...(commandFailed ? { failureCode } : {})
716
+ ...(failed ? { failureCode } : {})
691
717
  };
692
718
  }
693
719
 
@@ -821,11 +847,25 @@ export async function payAlipay402Resource(input, options = {}) {
821
847
  });
822
848
  }
823
849
 
824
- /** Queries an existing local payment attempt using the official query credential. */
850
+ /**
851
+ * Queries an existing local payment attempt using its official query
852
+ * credential. Alipay support can also provide a valid outShakeNo (often
853
+ * called a business order number); that one read-only query does not create
854
+ * local state, initiate a payment, or acknowledge fulfillment.
855
+ */
825
856
  export async function queryAlipay402Resource(input, options = {}) {
826
857
  const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
827
- const state = await readPaymentState(resourceUrl, options);
828
- if (!state?.queryCredential?.value) {
858
+ const suppliedOutShakeNo = input?.outShakeNo || input?.businessNo
859
+ ? normalizeOutShakeNo(input?.outShakeNo || input?.businessNo)
860
+ : null;
861
+ if (input?.outShakeNo && input?.businessNo && normalizeOutShakeNo(input.outShakeNo) !== normalizeOutShakeNo(input.businessNo)) {
862
+ throw new AlipayWalletError('ALIPAY_OUT_SHAKE_NO_CONFLICT', '提供的支付宝查询业务单号不一致。');
863
+ }
864
+ const persistedState = await readPaymentState(resourceUrl, options);
865
+ const queryCredential = suppliedOutShakeNo
866
+ ? { type: 'out_shake_no', value: suppliedOutShakeNo }
867
+ : persistedState?.queryCredential;
868
+ if (!queryCredential?.value) {
829
869
  return {
830
870
  ok: false,
831
871
  status: 'no_active_payment',
@@ -836,9 +876,20 @@ export async function queryAlipay402Resource(input, options = {}) {
836
876
  diagnostic: 'payment_state_not_found'
837
877
  };
838
878
  }
839
- const queryArg = state.queryCredential.type === 'out_shake_no'
840
- ? ['--out-shake-no', state.queryCredential.value]
841
- : ['-t', state.queryCredential.value];
879
+ const state = persistedState || {
880
+ version: 1,
881
+ resourceUrl,
882
+ resourceDigest: digest(resourceUrl),
883
+ sessionId: null,
884
+ paymentNeededPath: null,
885
+ queryCredential: null,
886
+ status: 'querying',
887
+ createdAt: new Date().toISOString(),
888
+ updatedAt: new Date().toISOString()
889
+ };
890
+ const queryArg = queryCredential.type === 'out_shake_no'
891
+ ? ['--out-shake-no', queryCredential.value]
892
+ : ['-t', queryCredential.value];
842
893
  const walletInvocation = await runOfficialCli([
843
894
  '402-query-payment-status',
844
895
  ...queryArg,
@@ -851,20 +902,24 @@ export async function queryAlipay402Resource(input, options = {}) {
851
902
  const officialMedia = ['pending_confirmation', 'completed'].includes(status)
852
903
  ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
853
904
  : null;
854
- state.status = status;
855
- state.queryCredential = result.credential || state.queryCredential;
856
- state.lastFailureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : null;
857
- state.lastDiagnosis = failed
858
- ? paymentFailureDiagnosis(state.lastFailureCode, walletInvocation.stdout, walletInvocation.stderr)
859
- : null;
860
- state.lastWalletDiagnostic = result.diagnostic;
861
- state.updatedAt = new Date().toISOString();
862
- await writePaymentState(state, options);
863
- if (status === 'completed') {
905
+ const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
906
+ if (persistedState) {
907
+ state.status = status;
908
+ // A support-supplied outShakeNo is used only for this invocation. Never
909
+ // replace or persist a local query credential with it.
910
+ state.queryCredential = suppliedOutShakeNo ? state.queryCredential : result.credential || state.queryCredential;
911
+ state.lastFailureCode = failureCode ?? null;
912
+ state.lastDiagnosis = failed
913
+ ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr)
914
+ : null;
915
+ state.lastWalletDiagnostic = result.diagnostic;
916
+ state.updatedAt = new Date().toISOString();
917
+ await writePaymentState(state, options);
918
+ }
919
+ if (status === 'completed' && persistedState) {
864
920
  const { paymentNeededPath } = statePaths(resourceUrl, options);
865
921
  await rm(paymentNeededPath, { force: true });
866
922
  }
867
- const failureCode = failed ? state.lastFailureCode : undefined;
868
923
  return publicPaymentResult(state, status, result.diagnostic, {
869
924
  commandFailed: failed,
870
925
  failureCode,
@@ -872,7 +927,8 @@ export async function queryAlipay402Resource(input, options = {}) {
872
927
  userVisible: ['pending_confirmation', 'completed'].includes(status)
873
928
  ? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
874
929
  : null,
875
- officialMedia
930
+ officialMedia,
931
+ ...(suppliedOutShakeNo ? { queryCredentialSource: 'supplied_out_shake_no' } : {})
876
932
  });
877
933
  }
878
934
 
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.27';
9
+ export const VERSION = '0.1.30';
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.28",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {