@xiashe/sdk 0.1.30 → 0.1.32

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,11 +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
- const OUT_SHAKE_NO_PATTERN = /^[A-Za-z0-9_-]{32}$/;
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'];
16
23
  const SAFE_OFFICIAL_FAILURE_CODES = new Set([
17
24
  'TRADE_PROGRESS_FAILED',
18
25
  'PAYMENT_NEEDED_REJECTED',
@@ -226,6 +233,22 @@ function normalizeIntentSummary(value) {
226
233
  return summary;
227
234
  }
228
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
+
229
252
  function stateDirectory(options = {}) {
230
253
  return path.resolve(options.stateDir || process.env.XIASHE_ALIPAY_PAYMENT_STATE_DIR || DEFAULT_STATE_DIR);
231
254
  }
@@ -297,7 +320,7 @@ function normalizedRunnerResult(result) {
297
320
  }
298
321
 
299
322
  async function runOfficialCli(args, options = {}) {
300
- 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);
301
324
  if (!command || /\s/.test(command)) {
302
325
  throw new AlipayWalletError('ALIPAY_BOT_COMMAND_INVALID', '支付宝钱包命令配置无效。');
303
326
  }
@@ -336,12 +359,17 @@ function walletStateFromOutput(stdout, stderr) {
336
359
  }
337
360
 
338
361
  function queryCredentialFromOutput(output) {
339
- // The official wallet has used both camelCase and snake_case JSON fields.
340
- // 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;
341
368
  const tradeNo = output.match(/(?:["']?trade[\s_-]*no["']?|交易(?:流水)?号)\s*["::=]\s*["']?([A-Za-z0-9._-]{8,128})/i)?.[1] || null;
342
- 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;
343
371
  // An outShakeNo is the preferred 402 status-query credential, but only its
344
- // documented 32-character AI Pay form may select that query path.
372
+ // documented 32-digit AI Pay form may select that query path.
345
373
  const validOutShakeNo = outShakeNo && OUT_SHAKE_NO_PATTERN.test(outShakeNo);
346
374
  return validOutShakeNo
347
375
  ? { type: 'out_shake_no', value: outShakeNo }
@@ -732,17 +760,28 @@ export async function checkAlipayWallet(options = {}) {
732
760
 
733
761
  /** Checks that the official buyer CLI is present, without querying wallet state. */
734
762
  export async function getAlipayWalletBridgeInfo(options = {}) {
735
- const { stdout, stderr } = await runOfficialCli(['--version'], options);
736
- 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;
737
772
  return {
738
- ok: true,
739
- state: 'runtime_available',
773
+ ok: contractValid,
774
+ state: contractValid ? 'runtime_available' : 'runtime_incompatible',
740
775
  provider: 'alipay-agent-payment',
741
- message: '已找到支付宝官方钱包程序;这只表示程序可执行,尚未检查钱包授权或交易资格。',
776
+ message: contractValid
777
+ ? '已找到支付宝官方钱包程序,并确认当前买方支付/查询命令参数可用;尚未检查钱包授权或交易资格。'
778
+ : '支付宝官方钱包程序缺少当前 402 买方支付或查询命令参数;未创建交易。请用官方安装器更新钱包运行时后重试。',
742
779
  walletStateChecked: false,
743
780
  transactionCreationVerified: false,
781
+ paymentCommandsValidated: contractValid,
744
782
  ...(runtimeVersion ? { runtimeVersion } : {}),
745
- 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}`)
746
785
  };
747
786
  }
748
787
 
@@ -758,6 +797,7 @@ export async function payAlipay402Resource(input, options = {}) {
758
797
  }
759
798
  const resourceUrl = normalizeResourceUrl(input?.resourceUrl);
760
799
  const intentSummary = normalizeIntentSummary(input?.intentSummary);
800
+ const sessionId = normalizeWalletSessionId(input?.sessionId);
761
801
  const resource = await fetchPaymentNeeded(resourceUrl, options);
762
802
  if (resource.alreadyPaid) {
763
803
  const state = (await readPaymentState(resourceUrl, options)) || {
@@ -784,7 +824,7 @@ export async function payAlipay402Resource(input, options = {}) {
784
824
  version: 1,
785
825
  resourceUrl,
786
826
  resourceDigest: digest(resourceUrl),
787
- sessionId: randomUUID(),
827
+ sessionId,
788
828
  paymentNeededPath: paths.paymentNeededPath,
789
829
  queryCredential: null,
790
830
  merchantResource: {
@@ -800,9 +840,9 @@ export async function payAlipay402Resource(input, options = {}) {
800
840
  const walletInvocation = await runOfficialCli([
801
841
  '402-buyer-pay',
802
842
  // The official buyer CLI accepts this long form of -s/-f/-r. The file
803
- // name is intentionally private and random; the exact Payment-Needed
804
- // content, original resource URL, and generated UUID session are what
805
- // 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.
806
846
  '--session-id', state.sessionId,
807
847
  '--file', state.paymentNeededPath,
808
848
  '--resource-url', resourceUrl,
@@ -889,7 +929,7 @@ export async function queryAlipay402Resource(input, options = {}) {
889
929
  };
890
930
  const queryArg = queryCredential.type === 'out_shake_no'
891
931
  ? ['--out-shake-no', queryCredential.value]
892
- : ['-t', queryCredential.value];
932
+ : ['--trade-no', queryCredential.value];
893
933
  const walletInvocation = await runOfficialCli([
894
934
  '402-query-payment-status',
895
935
  ...queryArg,
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.30';
9
+ export const VERSION = '0.1.32';
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.30",
3
+ "version": "0.1.32",
4
4
  "type": "module",
5
5
  "main": "index.mjs",
6
6
  "exports": {