@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 +79 -23
- package/index.mjs +1 -1
- package/package.json +1 -1
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 &&
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
...(
|
|
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
|
-
/**
|
|
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
|
|
828
|
-
|
|
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
|
|
840
|
-
|
|
841
|
-
|
|
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
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
:
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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