@xiashe/sdk 0.1.29 → 0.1.31

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
@@ -1,4 +1,4 @@
1
- import { createHash, randomUUID } from 'node:crypto';
1
+ import { createHash } from 'node:crypto';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { chmod, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
4
4
  import os from 'node:os';
@@ -8,10 +8,18 @@ const DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
8
8
  const RESOURCE_FETCH_TIMEOUT_MS = 20 * 1000;
9
9
  const MAX_OUTPUT_BYTES = 32 * 1024;
10
10
  const SENSITIVE_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_signature|runtimeToken|authorization\s*:/i;
11
- const SENSITIVE_USER_OUTPUT = /payment[-_ ]?proof|payment[-_ ]?needed|seller_signature|runtimeToken|authorization\s*:|trade[_ -]?no|交易(?:流水)?号|out[_ -]?shake|client[_ -]?session|payguard|validateclientparent|agentfamily/i;
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
13
  const OFFICIAL_MEDIA_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
14
14
  const MAX_OFFICIAL_MEDIA_BYTES = 8 * 1024 * 1024;
15
+ // The buyer CLI documents an AI Pay query number as 32 digits with `8282`
16
+ // in positions 11–14. Merchant out_trade_no values must never be accepted
17
+ // here: they cannot query a buyer-side 402 transaction.
18
+ const OUT_SHAKE_NO_PATTERN = /^\d{10}8282\d{18}$/;
19
+ const WALLET_SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,191}$/;
20
+ const OFFICIAL_WALLET_COMMAND = 'alipay-bot';
21
+ const REQUIRED_BUYER_PAY_OPTIONS = ['402-buyer-pay', '--session-id', '--file', '--resource-url', '--intent-summary'];
22
+ const REQUIRED_STATUS_OPTIONS = ['402-query-payment-status', '--out-shake-no', '--trade-no', '--resource-url'];
15
23
  const SAFE_OFFICIAL_FAILURE_CODES = new Set([
16
24
  'TRADE_PROGRESS_FAILED',
17
25
  'PAYMENT_NEEDED_REJECTED',
@@ -203,6 +211,20 @@ function normalizeResourceUrl(value) {
203
211
  return url.toString();
204
212
  }
205
213
 
214
+ /**
215
+ * Alipay support may call this a "business order number", but the 402 status
216
+ * API accepts it only when it has the 32-character outShakeNo shape accepted
217
+ * by the official client. This is a query credential, not a merchant out_trade_no and never a
218
+ * Payment-Proof or fulfillment acknowledgement credential.
219
+ */
220
+ function normalizeOutShakeNo(value) {
221
+ const outShakeNo = String(value || '').trim();
222
+ if (!OUT_SHAKE_NO_PATTERN.test(outShakeNo)) {
223
+ throw new AlipayWalletError('ALIPAY_OUT_SHAKE_NO_INVALID', '业务单号不是有效的支付宝 AI付查询凭证。');
224
+ }
225
+ return outShakeNo;
226
+ }
227
+
206
228
  function normalizeIntentSummary(value) {
207
229
  const summary = safeText(value, 180);
208
230
  if (!summary) {
@@ -211,6 +233,22 @@ function normalizeIntentSummary(value) {
211
233
  return summary;
212
234
  }
213
235
 
236
+ /**
237
+ * The official buyer CLI binds its payment intent to the active Agent
238
+ * conversation. A generated UUID is not a valid substitute: it prevents the
239
+ * wallet from correlating a later confirmation/status query with this task.
240
+ */
241
+ function normalizeWalletSessionId(value) {
242
+ const sessionId = String(value || '').trim();
243
+ if (!sessionId) {
244
+ throw new AlipayWalletError('ALIPAY_SESSION_ID_REQUIRED', '付款桥接缺少当前 Agent 的业务会话 ID;未请求资源或调用钱包。');
245
+ }
246
+ if (!WALLET_SESSION_ID_PATTERN.test(sessionId)) {
247
+ throw new AlipayWalletError('ALIPAY_SESSION_ID_INVALID', '付款桥接的业务会话 ID 格式无效;未请求资源或调用钱包。');
248
+ }
249
+ return sessionId;
250
+ }
251
+
214
252
  function stateDirectory(options = {}) {
215
253
  return path.resolve(options.stateDir || process.env.XIASHE_ALIPAY_PAYMENT_STATE_DIR || DEFAULT_STATE_DIR);
216
254
  }
@@ -282,7 +320,7 @@ function normalizedRunnerResult(result) {
282
320
  }
283
321
 
284
322
  async function runOfficialCli(args, options = {}) {
285
- const command = safeText(options.command || process.env.XIASHE_ALIPAY_BOT_COMMAND || 'alipay-bot', 200);
323
+ const command = safeText(options.command || process.env.XIASHE_ALIPAY_BOT_COMMAND || OFFICIAL_WALLET_COMMAND, 200);
286
324
  if (!command || /\s/.test(command)) {
287
325
  throw new AlipayWalletError('ALIPAY_BOT_COMMAND_INVALID', '支付宝钱包命令配置无效。');
288
326
  }
@@ -321,13 +359,18 @@ function walletStateFromOutput(stdout, stderr) {
321
359
  }
322
360
 
323
361
  function queryCredentialFromOutput(output) {
324
- // The official wallet has used both camelCase and snake_case JSON fields.
325
- // Match either spelling without forwarding the raw wallet output.
362
+ // Current official CLI output labels the preferred status credential as
363
+ // “订单号” or “查询单号”; it is not the merchant's out_trade_no. Older
364
+ // wallets used camelCase/snake_case fields, which remain supported here.
365
+ // Match only the documented, fixed AI Pay shape and keep it in private
366
+ // state—never return it in bridge output.
367
+ const labeledOutShakeNo = output.match(/(?:订单号|查询单号)\s*(?:["::=]\s*)?["']?(\d{32})/u)?.[1] || null;
326
368
  const tradeNo = output.match(/(?:["']?trade[\s_-]*no["']?|交易(?:流水)?号)\s*["::=]\s*["']?([A-Za-z0-9._-]{8,128})/i)?.[1] || null;
327
- const outShakeNo = output.match(/(?:["']?out[\s_-]*shake[\s_-]*no["']?|外部(?:摇一摇)?编号)\s*["::=]\s*["']?([A-Za-z0-9_-]{8,128})/i)?.[1] || null;
369
+ const structuredOutShakeNo = output.match(/(?:["']?out[\s_-]*shake[\s_-]*no["']?|外部(?:摇一摇)?编号)\s*["::=]\s*["']?(\d{32})/i)?.[1] || null;
370
+ const outShakeNo = labeledOutShakeNo || structuredOutShakeNo;
328
371
  // An outShakeNo is the preferred 402 status-query credential, but only its
329
- // documented 32-character AI Pay form may select that query path.
330
- const validOutShakeNo = outShakeNo && outShakeNo.length === 32 && outShakeNo.slice(10, 14) === '8282';
372
+ // documented 32-digit AI Pay form may select that query path.
373
+ const validOutShakeNo = outShakeNo && OUT_SHAKE_NO_PATTERN.test(outShakeNo);
331
374
  return validOutShakeNo
332
375
  ? { type: 'out_shake_no', value: outShakeNo }
333
376
  : tradeNo
@@ -343,9 +386,13 @@ function paymentStatusFromOutput(stdout, stderr) {
343
386
  // Treat an explicit unsuccessful result as failed so callers retain the
344
387
  // normalized failure category instead of misleadingly reporting an unknown
345
388
  // wallet state.
346
- const failed = /支付失败|交易失败|payment\s+failed|"success"\s*:\s*false|"errorCode"\s*:/i.test(output);
389
+ // A successful status-query transport can still include the protected
390
+ // resource's safe ALIPAY_VERIFY_* response. That is a merchant-side proof
391
+ // verification failure, not an unknown wallet state.
392
+ const merchantDiagnostic = safeMerchantDiagnosticCode(stdout, stderr);
393
+ const failed = Boolean(merchantDiagnostic) || /支付失败|交易失败|payment\s+failed|"success"\s*:\s*false|"errorCode"\s*:/i.test(output);
347
394
  return {
348
- status: completed ? 'completed' : pending ? 'pending_confirmation' : failed ? 'failed' : 'unknown',
395
+ status: failed ? 'failed' : completed ? 'completed' : pending ? 'pending_confirmation' : 'unknown',
349
396
  credential: queryCredentialFromOutput(output),
350
397
  diagnostic: safeDiagnostic(output)
351
398
  };
@@ -379,6 +426,7 @@ async function officialWalletMedia(stdout, stderr) {
379
426
 
380
427
  function walletFailureCode(stdout, stderr, exitCode) {
381
428
  const output = `${stdout}\n${stderr}`;
429
+ if (safeMerchantDiagnosticCode(stdout, stderr)) return 'merchant_payment_proof_verification_failed';
382
430
  if (/TRADE_PROGRESS_FAILED/i.test(output)) return 'wallet_trade_progress_failed';
383
431
  if (/payment[_ -]?needed[_ -]?rejected|账单.*(?:被拒绝|拒绝)|bill.*(?:rejected|denied)/i.test(output)) return 'payment_needed_rejected';
384
432
  if (/余额不足|insufficient\s+balance/i.test(output)) return 'insufficient_balance';
@@ -501,7 +549,9 @@ function paymentFailureDiagnosis(failureCode, stdout, stderr) {
501
549
 
502
550
  function paymentFailureUserVisible(diagnosis) {
503
551
  const lines = [
504
- diagnosis.code === 'wallet_query_credential_missing'
552
+ diagnosis.stage === 'merchant_payment_proof_verification'
553
+ ? '商家未能验证本次支付宝付款凭证,尚未授予付费 Skill 权益。'
554
+ : diagnosis.code === 'wallet_query_credential_missing'
505
555
  ? '支付宝钱包未返回可继续查询的交易凭据,当前付款状态无法确认。'
506
556
  : '支付宝付款未创建可确认交易。',
507
557
  `诊断代码:${diagnosis.code}`,
@@ -525,6 +575,9 @@ function paymentMessage(status) {
525
575
  }
526
576
 
527
577
  function paymentFailureMessage(failureCode) {
578
+ if (failureCode === 'merchant_payment_proof_verification_failed') {
579
+ return '商家未能验证支付宝付款凭证,未授予付费 Skill 权益;请勿重放旧凭证或重复付款。';
580
+ }
528
581
  if (failureCode === 'wallet_query_credential_missing') {
529
582
  return '支付宝钱包返回待确认,但未返回可继续查询的交易凭据。无法确认是否已付款,因此不会授予付费 Skill 权益;请勿再次付款。';
530
583
  }
@@ -684,10 +737,11 @@ function publicPaymentResult(state, status, diagnostic, options = {}) {
684
737
  ...(failed && diagnosis ? { userVisible: paymentFailureUserVisible(diagnosis), diagnosis } : {}),
685
738
  ...(!failed && options.userVisible ? { userVisible: options.userVisible } : {}),
686
739
  ...(options.officialMedia ? { officialMedia: options.officialMedia } : {}),
740
+ ...(options.queryCredentialSource ? { queryCredentialSource: options.queryCredentialSource } : {}),
687
741
  ...(status === 'pending_confirmation'
688
742
  ? { confirmation: { source: 'official_alipay_wallet', surface: 'app_or_official_media', required: true } }
689
743
  : { confirmation: { source: 'official_alipay_wallet', required: false } }),
690
- ...(commandFailed ? { failureCode } : {})
744
+ ...(failed ? { failureCode } : {})
691
745
  };
692
746
  }
693
747
 
@@ -706,17 +760,28 @@ export async function checkAlipayWallet(options = {}) {
706
760
 
707
761
  /** Checks that the official buyer CLI is present, without querying wallet state. */
708
762
  export async function getAlipayWalletBridgeInfo(options = {}) {
709
- const { stdout, stderr } = await runOfficialCli(['--version'], options);
710
- const runtimeVersion = safeRuntimeVersion(stdout, stderr);
763
+ const version = await runOfficialCli(['--version'], options);
764
+ const buyerPayHelp = await runOfficialCli(['402-buyer-pay', '--help'], options);
765
+ const statusHelp = await runOfficialCli(['402-query-payment-status', '--help'], options);
766
+ const runtimeVersion = safeRuntimeVersion(version.stdout, version.stderr);
767
+ const buyerPayOutput = `${buyerPayHelp.stdout}\n${buyerPayHelp.stderr}`;
768
+ const statusOutput = `${statusHelp.stdout}\n${statusHelp.stderr}`;
769
+ const buyerPaySupported = REQUIRED_BUYER_PAY_OPTIONS.every((option) => buyerPayOutput.includes(option));
770
+ const statusSupported = REQUIRED_STATUS_OPTIONS.every((option) => statusOutput.includes(option));
771
+ const contractValid = buyerPaySupported && statusSupported;
711
772
  return {
712
- ok: true,
713
- state: 'runtime_available',
773
+ ok: contractValid,
774
+ state: contractValid ? 'runtime_available' : 'runtime_incompatible',
714
775
  provider: 'alipay-agent-payment',
715
- message: '已找到支付宝官方钱包程序;这只表示程序可执行,尚未检查钱包授权或交易资格。',
776
+ message: contractValid
777
+ ? '已找到支付宝官方钱包程序,并确认当前买方支付/查询命令参数可用;尚未检查钱包授权或交易资格。'
778
+ : '支付宝官方钱包程序缺少当前 402 买方支付或查询命令参数;未创建交易。请用官方安装器更新钱包运行时后重试。',
716
779
  walletStateChecked: false,
717
780
  transactionCreationVerified: false,
781
+ paymentCommandsValidated: contractValid,
718
782
  ...(runtimeVersion ? { runtimeVersion } : {}),
719
- diagnostic: safeDiagnostic(stdout || stderr)
783
+ ...(contractValid ? {} : { failureCode: 'wallet_runtime_contract_invalid' }),
784
+ diagnostic: safeDiagnostic(`${version.stdout}\n${version.stderr}\n${buyerPayHelp.stdout}\n${buyerPayHelp.stderr}\n${statusHelp.stdout}\n${statusHelp.stderr}`)
720
785
  };
721
786
  }
722
787
 
@@ -732,6 +797,7 @@ export async function payAlipay402Resource(input, options = {}) {
732
797
  }
733
798
  const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
734
799
  const intentSummary = normalizeIntentSummary(input?.intentSummary);
800
+ const sessionId = normalizeWalletSessionId(input?.sessionId);
735
801
  const resource = await fetchPaymentNeeded(resourceUrl, options);
736
802
  if (resource.alreadyPaid) {
737
803
  const state = (await readPaymentState(resourceUrl, options)) || {
@@ -758,7 +824,7 @@ export async function payAlipay402Resource(input, options = {}) {
758
824
  version: 1,
759
825
  resourceUrl,
760
826
  resourceDigest: digest(resourceUrl),
761
- sessionId: randomUUID(),
827
+ sessionId,
762
828
  paymentNeededPath: paths.paymentNeededPath,
763
829
  queryCredential: null,
764
830
  merchantResource: {
@@ -774,9 +840,9 @@ export async function payAlipay402Resource(input, options = {}) {
774
840
  const walletInvocation = await runOfficialCli([
775
841
  '402-buyer-pay',
776
842
  // The official buyer CLI accepts this long form of -s/-f/-r. The file
777
- // name is intentionally private and random; the exact Payment-Needed
778
- // content, original resource URL, and generated UUID session are what
779
- // define the payment request.
843
+ // name is intentionally private and random. The exact Payment-Needed
844
+ // content, original resource URL, and active Agent business session are
845
+ // what define the payment request.
780
846
  '--session-id', state.sessionId,
781
847
  '--file', state.paymentNeededPath,
782
848
  '--resource-url', resourceUrl,
@@ -821,11 +887,25 @@ export async function payAlipay402Resource(input, options = {}) {
821
887
  });
822
888
  }
823
889
 
824
- /** Queries an existing local payment attempt using the official query credential. */
890
+ /**
891
+ * Queries an existing local payment attempt using its official query
892
+ * credential. Alipay support can also provide a valid outShakeNo (often
893
+ * called a business order number); that one read-only query does not create
894
+ * local state, initiate a payment, or acknowledge fulfillment.
895
+ */
825
896
  export async function queryAlipay402Resource(input, options = {}) {
826
897
  const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
827
- const state = await readPaymentState(resourceUrl, options);
828
- if (!state?.queryCredential?.value) {
898
+ const suppliedOutShakeNo = input?.outShakeNo || input?.businessNo
899
+ ? normalizeOutShakeNo(input?.outShakeNo || input?.businessNo)
900
+ : null;
901
+ if (input?.outShakeNo && input?.businessNo && normalizeOutShakeNo(input.outShakeNo) !== normalizeOutShakeNo(input.businessNo)) {
902
+ throw new AlipayWalletError('ALIPAY_OUT_SHAKE_NO_CONFLICT', '提供的支付宝查询业务单号不一致。');
903
+ }
904
+ const persistedState = await readPaymentState(resourceUrl, options);
905
+ const queryCredential = suppliedOutShakeNo
906
+ ? { type: 'out_shake_no', value: suppliedOutShakeNo }
907
+ : persistedState?.queryCredential;
908
+ if (!queryCredential?.value) {
829
909
  return {
830
910
  ok: false,
831
911
  status: 'no_active_payment',
@@ -836,9 +916,20 @@ export async function queryAlipay402Resource(input, options = {}) {
836
916
  diagnostic: 'payment_state_not_found'
837
917
  };
838
918
  }
839
- const queryArg = state.queryCredential.type === 'out_shake_no'
840
- ? ['--out-shake-no', state.queryCredential.value]
841
- : ['-t', state.queryCredential.value];
919
+ const state = persistedState || {
920
+ version: 1,
921
+ resourceUrl,
922
+ resourceDigest: digest(resourceUrl),
923
+ sessionId: null,
924
+ paymentNeededPath: null,
925
+ queryCredential: null,
926
+ status: 'querying',
927
+ createdAt: new Date().toISOString(),
928
+ updatedAt: new Date().toISOString()
929
+ };
930
+ const queryArg = queryCredential.type === 'out_shake_no'
931
+ ? ['--out-shake-no', queryCredential.value]
932
+ : ['--trade-no', queryCredential.value];
842
933
  const walletInvocation = await runOfficialCli([
843
934
  '402-query-payment-status',
844
935
  ...queryArg,
@@ -851,20 +942,24 @@ export async function queryAlipay402Resource(input, options = {}) {
851
942
  const officialMedia = ['pending_confirmation', 'completed'].includes(status)
852
943
  ? await officialWalletMedia(walletInvocation.stdout, walletInvocation.stderr)
853
944
  : 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') {
945
+ const failureCode = failed ? walletFailureCode(walletInvocation.stdout, walletInvocation.stderr, walletInvocation.exitCode) : undefined;
946
+ if (persistedState) {
947
+ state.status = status;
948
+ // A support-supplied outShakeNo is used only for this invocation. Never
949
+ // replace or persist a local query credential with it.
950
+ state.queryCredential = suppliedOutShakeNo ? state.queryCredential : result.credential || state.queryCredential;
951
+ state.lastFailureCode = failureCode ?? null;
952
+ state.lastDiagnosis = failed
953
+ ? paymentFailureDiagnosis(failureCode, walletInvocation.stdout, walletInvocation.stderr)
954
+ : null;
955
+ state.lastWalletDiagnostic = result.diagnostic;
956
+ state.updatedAt = new Date().toISOString();
957
+ await writePaymentState(state, options);
958
+ }
959
+ if (status === 'completed' && persistedState) {
864
960
  const { paymentNeededPath } = statePaths(resourceUrl, options);
865
961
  await rm(paymentNeededPath, { force: true });
866
962
  }
867
- const failureCode = failed ? state.lastFailureCode : undefined;
868
963
  return publicPaymentResult(state, status, result.diagnostic, {
869
964
  commandFailed: failed,
870
965
  failureCode,
@@ -872,7 +967,8 @@ export async function queryAlipay402Resource(input, options = {}) {
872
967
  userVisible: ['pending_confirmation', 'completed'].includes(status)
873
968
  ? userVisibleWalletOutput(walletInvocation.stdout, walletInvocation.stderr)
874
969
  : null,
875
- officialMedia
970
+ officialMedia,
971
+ ...(suppliedOutShakeNo ? { queryCredentialSource: 'supplied_out_shake_no' } : {})
876
972
  });
877
973
  }
878
974
 
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.29';
9
+ export const VERSION = '0.1.31';
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.29",
3
+ "version": "0.1.31",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {