@harrylabsj/kiwi 0.6.0 → 0.6.2

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.
Files changed (167) hide show
  1. package/README.md +21 -0
  2. package/contracts/candidate-agent-dto-1.0.README.md +12 -8
  3. package/contracts/conformance/python-service/README.md +19 -0
  4. package/contracts/conformance/python-service/run.py +134 -0
  5. package/contracts/kiwi-catalog/1.0/listing-record.schema.json +11 -1
  6. package/contracts/kiwi-contracts.lock.json +7 -0
  7. package/contracts/manifest.json +61 -0
  8. package/contracts/vectors/candidate-agent.invalid-private-field.json +7 -0
  9. package/contracts/vectors/candidate-agent.valid.json +26 -0
  10. package/dist/a2a/client/client.js +39 -34
  11. package/dist/a2a/client/client.js.map +1 -1
  12. package/dist/a2a/client/url-policy.d.ts +8 -1
  13. package/dist/a2a/client/url-policy.js +18 -2
  14. package/dist/a2a/client/url-policy.js.map +1 -1
  15. package/dist/a2a/clock.d.ts +30 -0
  16. package/dist/a2a/clock.js +39 -0
  17. package/dist/a2a/clock.js.map +1 -0
  18. package/dist/a2a/negotiate.d.ts +6 -0
  19. package/dist/a2a/negotiate.js +10 -12
  20. package/dist/a2a/negotiate.js.map +1 -1
  21. package/dist/a2a/node.d.ts +23 -3
  22. package/dist/a2a/node.js +128 -18
  23. package/dist/a2a/node.js.map +1 -1
  24. package/dist/a2a/server/merchant-handler.d.ts +4 -0
  25. package/dist/a2a/server/merchant-handler.js +238 -25
  26. package/dist/a2a/server/merchant-handler.js.map +1 -1
  27. package/dist/a2a/server/pipeline.d.ts +17 -0
  28. package/dist/a2a/server/pipeline.js +89 -1
  29. package/dist/a2a/server/pipeline.js.map +1 -1
  30. package/dist/a2a/server/ucp.d.ts +30 -2
  31. package/dist/a2a/server/ucp.js +23 -14
  32. package/dist/a2a/server/ucp.js.map +1 -1
  33. package/dist/agent/agent-db.js +5 -0
  34. package/dist/agent/agent-db.js.map +1 -1
  35. package/dist/agent/buyer/buyer-tools.d.ts +5 -0
  36. package/dist/agent/buyer/buyer-tools.js +8 -1
  37. package/dist/agent/buyer/buyer-tools.js.map +1 -1
  38. package/dist/agent/buyer/types.d.ts +0 -25
  39. package/dist/agent/buyer/types.js.map +1 -1
  40. package/dist/agent/kernel-builder.d.ts +27 -0
  41. package/dist/agent/kernel-builder.js +180 -0
  42. package/dist/agent/kernel-builder.js.map +1 -0
  43. package/dist/agent/kernel.d.ts +6 -0
  44. package/dist/agent/kernel.js +95 -8
  45. package/dist/agent/kernel.js.map +1 -1
  46. package/dist/agent/memory/store.d.ts +5 -0
  47. package/dist/agent/memory/store.js +29 -1
  48. package/dist/agent/memory/store.js.map +1 -1
  49. package/dist/agent/merchant/action-candidate.js +15 -1
  50. package/dist/agent/merchant/action-candidate.js.map +1 -1
  51. package/dist/agent/merchant/merchant-client.js +22 -4
  52. package/dist/agent/merchant/merchant-client.js.map +1 -1
  53. package/dist/agent/merchant/types.d.ts +5 -2
  54. package/dist/agent/merchant/types.js +6 -2
  55. package/dist/agent/merchant/types.js.map +1 -1
  56. package/dist/agent/negotiation-chat.js +34 -14
  57. package/dist/agent/negotiation-chat.js.map +1 -1
  58. package/dist/cli.d.ts +56 -0
  59. package/dist/cli.js +141 -156
  60. package/dist/cli.js.map +1 -1
  61. package/dist/commerce/data-source.d.ts +7 -3
  62. package/dist/commerce/data-source.js.map +1 -1
  63. package/dist/commerce/http-client.js +35 -31
  64. package/dist/commerce/http-client.js.map +1 -1
  65. package/dist/commerce/shopping-cli-source.js +66 -48
  66. package/dist/commerce/shopping-cli-source.js.map +1 -1
  67. package/dist/config/profile.d.ts +7 -0
  68. package/dist/config/profile.js +23 -0
  69. package/dist/config/profile.js.map +1 -1
  70. package/dist/counterparty/a2a-direct/index.d.ts +5 -0
  71. package/dist/counterparty/a2a-direct/index.js +24 -0
  72. package/dist/counterparty/a2a-direct/index.js.map +1 -1
  73. package/dist/counterparty/shopping-cli-hosted/index.js +52 -19
  74. package/dist/counterparty/shopping-cli-hosted/index.js.map +1 -1
  75. package/dist/discovery/catalog-source/kiwi-record.d.ts +3 -0
  76. package/dist/discovery/catalog-source/kiwi-record.js.map +1 -1
  77. package/dist/discovery/catalog-source/kiwi-source.js +46 -36
  78. package/dist/discovery/catalog-source/kiwi-source.js.map +1 -1
  79. package/dist/discovery/catalog-source/register.d.ts +6 -0
  80. package/dist/discovery/catalog-source/register.js +27 -8
  81. package/dist/discovery/catalog-source/register.js.map +1 -1
  82. package/dist/discovery/catalog-source/source.js +39 -35
  83. package/dist/discovery/catalog-source/source.js.map +1 -1
  84. package/dist/discovery/resolve.d.ts +15 -0
  85. package/dist/discovery/resolve.js +59 -35
  86. package/dist/discovery/resolve.js.map +1 -1
  87. package/dist/discovery/ucp/index.d.ts +1 -1
  88. package/dist/discovery/ucp/index.js +1 -1
  89. package/dist/discovery/ucp/index.js.map +1 -1
  90. package/dist/discovery/ucp/resolver.d.ts +5 -0
  91. package/dist/discovery/ucp/resolver.js +65 -38
  92. package/dist/discovery/ucp/resolver.js.map +1 -1
  93. package/dist/discovery/ucp/vendor.d.ts +1 -1
  94. package/dist/discovery/ucp/vendor.js +1 -1
  95. package/dist/handoff/delivery.d.ts +4 -2
  96. package/dist/handoff/delivery.js +6 -1
  97. package/dist/handoff/delivery.js.map +1 -1
  98. package/dist/handoff/errors.d.ts +1 -1
  99. package/dist/handoff/errors.js +1 -0
  100. package/dist/handoff/errors.js.map +1 -1
  101. package/dist/handoff/idempotency.d.ts +10 -1
  102. package/dist/handoff/idempotency.js +13 -3
  103. package/dist/handoff/idempotency.js.map +1 -1
  104. package/dist/handoff/index.d.ts +1 -1
  105. package/dist/handoff/index.js +1 -1
  106. package/dist/handoff/index.js.map +1 -1
  107. package/dist/handoff/ledger.d.ts +9 -0
  108. package/dist/handoff/ledger.js +66 -0
  109. package/dist/handoff/ledger.js.map +1 -1
  110. package/dist/handoff/transaction.d.ts +21 -3
  111. package/dist/handoff/transaction.js +50 -11
  112. package/dist/handoff/transaction.js.map +1 -1
  113. package/dist/handoff/ucp-checkout/channel.js +15 -10
  114. package/dist/handoff/ucp-checkout/channel.js.map +1 -1
  115. package/dist/negotiation/jcs.js +11 -8
  116. package/dist/negotiation/jcs.js.map +1 -1
  117. package/dist/negotiation/lease/store.d.ts +25 -0
  118. package/dist/negotiation/lease/store.js +101 -0
  119. package/dist/negotiation/lease/store.js.map +1 -0
  120. package/dist/negotiation/recovery/recover.js +25 -1
  121. package/dist/negotiation/recovery/recover.js.map +1 -1
  122. package/dist/negotiation/state/phase.js +12 -4
  123. package/dist/negotiation/state/phase.js.map +1 -1
  124. package/dist/operator/runner.d.ts +11 -0
  125. package/dist/operator/runner.js +20 -1
  126. package/dist/operator/runner.js.map +1 -1
  127. package/dist/product-catalog.d.ts +43 -0
  128. package/dist/product-catalog.js +60 -0
  129. package/dist/product-catalog.js.map +1 -0
  130. package/dist/product-cli.d.ts +1 -1
  131. package/dist/product-cli.js +1 -1
  132. package/dist/product-publish.d.ts +15 -17
  133. package/dist/product-publish.js +279 -46
  134. package/dist/product-publish.js.map +1 -1
  135. package/dist/supervisor/stack-config.d.ts +6 -1
  136. package/dist/supervisor/stack-config.js +11 -3
  137. package/dist/supervisor/stack-config.js.map +1 -1
  138. package/dist/trust/identity/message-signature.js +17 -0
  139. package/dist/trust/identity/message-signature.js.map +1 -1
  140. package/dist/weixin/channel.d.ts +89 -0
  141. package/dist/weixin/channel.js +370 -0
  142. package/dist/weixin/channel.js.map +1 -0
  143. package/dist/weixin/cli-weixin.d.ts +29 -0
  144. package/dist/weixin/cli-weixin.js +130 -0
  145. package/dist/weixin/cli-weixin.js.map +1 -0
  146. package/dist/weixin/credentials.d.ts +33 -0
  147. package/dist/weixin/credentials.js +107 -0
  148. package/dist/weixin/credentials.js.map +1 -0
  149. package/dist/weixin/ilink-client.d.ts +44 -0
  150. package/dist/weixin/ilink-client.js +287 -0
  151. package/dist/weixin/ilink-client.js.map +1 -0
  152. package/dist/weixin/login.d.ts +37 -0
  153. package/dist/weixin/login.js +93 -0
  154. package/dist/weixin/login.js.map +1 -0
  155. package/dist/weixin/qr.d.ts +26 -0
  156. package/dist/weixin/qr.js +525 -0
  157. package/dist/weixin/qr.js.map +1 -0
  158. package/dist/weixin/types.d.ts +114 -0
  159. package/dist/weixin/types.js +48 -0
  160. package/dist/weixin/types.js.map +1 -0
  161. package/package.json +12 -2
  162. package/dist/commerce/erp-source.d.ts +0 -51
  163. package/dist/commerce/erp-source.js +0 -184
  164. package/dist/commerce/erp-source.js.map +0 -1
  165. package/dist/commerce/local-db-source.d.ts +0 -50
  166. package/dist/commerce/local-db-source.js +0 -162
  167. package/dist/commerce/local-db-source.js.map +0 -1
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Copyright 2026 harrylabsj
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import type { BotCredentials } from "./types.js";
17
+ import type { IlinkClient } from "./ilink-client.js";
18
+ export interface LoginOptions {
19
+ /** 二维码渲染输出(缺省打印到 stdout)。 */
20
+ render?: (qr: string[]) => void;
21
+ /** 提示行输出(缺省 stderr)。 */
22
+ notice?: (line: string) => void;
23
+ /** 扫码轮询间隔(毫秒;缺省 DEFAULT_WEIXIN_TIMINGS.qrPollMs)。 */
24
+ qrPollMs?: number;
25
+ /** 过期刷新上限(缺省 DEFAULT_WEIXIN_TIMINGS.qrRefreshCap)。 */
26
+ qrRefreshCap?: number;
27
+ /** 二维码每模块列数(审查 P3:此前 CLI --qr-scale 传到这里就断了,
28
+ * renderQr 恒用默认 scale=1,文档承诺的 --qr-scale 2 静默无效)。 */
29
+ qrScale?: number;
30
+ /** 时间源(测试注入)。 */
31
+ now?: () => string;
32
+ }
33
+ /**
34
+ * 执行扫码登录并返回凭证(调用方负责持久化)。
35
+ * 失败:网络/协议错抛 WeixinError;多次过期抛 qr_expired。
36
+ */
37
+ export declare function loginWithQrcode(client: IlinkClient, options?: LoginOptions): Promise<BotCredentials>;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Copyright 2026 harrylabsj
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * 微信扫码登录流程(iLink Bot API)——取码 → 渲染 QR → 轮询状态 →
18
+ * confirmed 落盘凭证。expired 自动刷新(≤3 次);scaned_but_redirect
19
+ * 切换 base URL。登录成功后向配对用户发一条"通道就绪"提示(协议上
20
+ * 首次主动消息可能不可达——用户先发一句任意消息即"唤醒")。
21
+ */
22
+ import { renderQr } from "./qr.js";
23
+ import { DEFAULT_WEIXIN_TIMINGS, WeixinError } from "./types.js";
24
+ /**
25
+ * 执行扫码登录并返回凭证(调用方负责持久化)。
26
+ * 失败:网络/协议错抛 WeixinError;多次过期抛 qr_expired。
27
+ */
28
+ export async function loginWithQrcode(client, options = {}) {
29
+ const render = options.render ?? ((qr) => {
30
+ for (const line of qr)
31
+ process.stdout.write(`${line}\n`);
32
+ });
33
+ const notice = options.notice ?? ((line) => process.stderr.write(`${line}\n`));
34
+ const pollMs = options.qrPollMs ?? DEFAULT_WEIXIN_TIMINGS.qrPollMs;
35
+ const refreshCap = options.qrRefreshCap ?? DEFAULT_WEIXIN_TIMINGS.qrRefreshCap;
36
+ const now = options.now ?? (() => new Date().toISOString());
37
+ let refreshCount = 0;
38
+ for (;;) {
39
+ const qrcode = await client.getBotQrcode();
40
+ render(renderQr(qrcode.qrcode_img_content, options.qrScale ?? 1));
41
+ notice("请使用微信扫一扫登录 kiwi 微信通道(8 分钟内有效,过期自动刷新)");
42
+ // 轮询状态直到 confirmed / expired(自动刷新)
43
+ const credentials = await pollUntilConfirmed(client, qrcode.qrcode, {
44
+ pollMs,
45
+ notice,
46
+ onExpired: async () => {
47
+ refreshCount += 1;
48
+ if (refreshCount > refreshCap) {
49
+ throw new WeixinError("qr_expired", `二维码多次过期(>${refreshCap} 次),请重新执行 kiwi weixin`);
50
+ }
51
+ notice(`二维码已过期,正在刷新(${refreshCount}/${refreshCap})…`);
52
+ return true; // 外层 for 循环重新取码
53
+ },
54
+ });
55
+ if (credentials !== null) {
56
+ credentials.saved_at = now();
57
+ notice("微信连接成功,等待消息…(首次请先发一句任意消息唤醒通道)");
58
+ return credentials;
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * 轮询扫码状态直到 confirmed;expired 时回调 onExpired(返回 true 重新取码)。
64
+ * 返回 null 表示 expired 且已请求刷新(由调用方重新取码)。
65
+ */
66
+ async function pollUntilConfirmed(client, qrcodeHex, options) {
67
+ for (;;) {
68
+ const status = await client.getQrcodeStatus(qrcodeHex);
69
+ switch (status.state) {
70
+ case "wait":
71
+ break;
72
+ case "scaned":
73
+ options.notice("已扫码,请在微信上确认登录…");
74
+ break;
75
+ case "scaned_but_redirect": {
76
+ options.notice(`切换服务节点 ${status.baseUrl}`);
77
+ client.setBaseUrl(status.baseUrl);
78
+ break;
79
+ }
80
+ case "expired": {
81
+ const shouldRefresh = await options.onExpired();
82
+ return shouldRefresh ? null : null;
83
+ }
84
+ case "confirmed":
85
+ return status.credentials;
86
+ }
87
+ await sleep(options.pollMs);
88
+ }
89
+ }
90
+ function sleep(ms) {
91
+ return new Promise((resolve) => setTimeout(resolve, ms));
92
+ }
93
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/weixin/login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAoBjE;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAmB,EACnB,UAAwB,EAAE;IAE1B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,EAAY,EAAE,EAAE;QACjD,KAAK,MAAM,IAAI,IAAI,EAAE;YAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;IACvF,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,IAAI,sBAAsB,CAAC,QAAQ,CAAC;IACnE,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,IAAI,sBAAsB,CAAC,YAAY,CAAC;IAC/E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IAE5D,IAAI,YAAY,GAAG,CAAC,CAAC;IACrB,SAAS,CAAC;QACR,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,EAAE,CAAC;QAC3C,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,kBAAkB,EAAE,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;QAClE,MAAM,CAAC,sCAAsC,CAAC,CAAC;QAE/C,mCAAmC;QACnC,MAAM,WAAW,GAAG,MAAM,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE;YAClE,MAAM;YACN,MAAM;YACN,SAAS,EAAE,KAAK,IAAsB,EAAE;gBACtC,YAAY,IAAI,CAAC,CAAC;gBAClB,IAAI,YAAY,GAAG,UAAU,EAAE,CAAC;oBAC9B,MAAM,IAAI,WAAW,CAAC,YAAY,EAAE,YAAY,UAAU,uBAAuB,CAAC,CAAC;gBACrF,CAAC;gBACD,MAAM,CAAC,eAAe,YAAY,IAAI,UAAU,IAAI,CAAC,CAAC;gBACtD,OAAO,IAAI,CAAC,CAAC,gBAAgB;YAC/B,CAAC;SACF,CAAC,CAAC;QACH,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;YACzB,WAAW,CAAC,QAAQ,GAAG,GAAG,EAAE,CAAC;YAC7B,MAAM,CAAC,+BAA+B,CAAC,CAAC;YACxC,OAAO,WAAW,CAAC;QACrB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,kBAAkB,CAC/B,MAAmB,EACnB,SAAiB,EACjB,OAIC;IAED,SAAS,CAAC;QACR,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QACvD,QAAQ,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,KAAK,MAAM;gBACT,MAAM;YACR,KAAK,QAAQ;gBACX,OAAO,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;gBACjC,MAAM;YACR,KAAK,qBAAqB,CAAC,CAAC,CAAC;gBAC3B,OAAO,CAAC,MAAM,CAAC,UAAU,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC3C,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAClC,MAAM;YACR,CAAC;YACD,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,CAAC;gBAChD,OAAO,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YACrC,CAAC;YACD,KAAK,WAAW;gBACd,OAAO,MAAM,CAAC,WAAW,CAAC;QAC9B,CAAC;QACD,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,EAAU;IACvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright 2026 harrylabsj
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /** 掩码 0–7 判定(x, y 为模块坐标)。 */
17
+ export declare function maskPattern(mask: number, x: number, y: number): boolean;
18
+ /**
19
+ * 生成 QR 码矩阵(1 = 暗模块)。版本自动选择(1–10,纠错 L),
20
+ * 8 掩码全试 penalty 最低胜出。超长(>271 字节)抛 WeixinError("validation")。
21
+ */
22
+ export declare function encodeQrMatrix(payload: string): Uint8Array;
23
+ /** 终端渲染:半块字符(00→" "、01→"▀"、10→"▄"、11→"█"),静区 4。 */
24
+ export declare function renderQrMatrix(matrix: Uint8Array, size: number, scale?: number): string[];
25
+ /** 便捷入口:payload → 渲染行(静区已含,可直接 stdout)。 */
26
+ export declare function renderQr(payload: string, scale?: number): string[];
@@ -0,0 +1,525 @@
1
+ /**
2
+ * Copyright 2026 harrylabsj
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * QR 编码器(零运行时依赖,纯函数)——微信扫码登录的终端二维码渲染。
18
+ *
19
+ * 规格:ISO/IEC 18004,Byte mode,纠错级别 L,版本 1–10。
20
+ * 流水线:版本选择 → 位流(模式/长度/数据/终结/填充)→ Reed-Solomon
21
+ * (GF(2^8) 0x11D,多块交织)→ 矩阵放置(finder/timing/alignment/dark/
22
+ * 数据 zigzag)→ 8 掩码全试(penalty N1–N4 最低胜出)→ format/version 信息。
23
+ * 正确性由测试双重验证:qrcode 库矩阵对照 + jsqr 解码回环。
24
+ */
25
+ import { WeixinError } from "./types.js";
26
+ /** 版本容量表(字节,纠错 L;索引 = 版本号 1..10)。 */
27
+ const CAPACITY_BYTE_L = [
28
+ 17, 32, 53, 78, 106, 134, 154, 192, 230, 271,
29
+ ];
30
+ /** 总码字数(含 ECC;索引 = 版本号 1..10)。 */
31
+ const TOTAL_CODEWORDS = [
32
+ 26, 44, 70, 100, 134, 172, 196, 242, 292, 346,
33
+ ];
34
+ /** 每块 ECC 码字数(纠错 L;索引 = 版本号 1..10)。 */
35
+ const ECC_PER_BLOCK_L = [
36
+ 7, 10, 15, 20, 26, 18, 20, 24, 30, 36,
37
+ ];
38
+ /** 块数(纠错 L;索引 = 版本号 1..10)。 */
39
+ const BLOCKS_L = [1, 1, 1, 1, 1, 2, 2, 2, 2, 2];
40
+ /** 对齐图案中心坐标(索引 = 版本号 1..10;v1 无)。 */
41
+ const ALIGNMENT_PATTERNS = [
42
+ [],
43
+ [6, 18],
44
+ [6, 22],
45
+ [6, 26],
46
+ [6, 30],
47
+ [6, 34],
48
+ [6, 22, 38],
49
+ [6, 24, 42],
50
+ [6, 26, 46],
51
+ [6, 28, 50],
52
+ ];
53
+ // ── GF(2^8) 有限域(本原多项式 0x11D)──────────────────────────────────
54
+ const GF_EXP = new Uint8Array(512);
55
+ const GF_LOG = new Uint8Array(256);
56
+ {
57
+ let x = 1;
58
+ for (let i = 0; i < 255; i++) {
59
+ GF_EXP[i] = x;
60
+ GF_LOG[x] = i;
61
+ x <<= 1;
62
+ if (x & 0x100)
63
+ x ^= 0x11d;
64
+ }
65
+ for (let i = 255; i < 512; i++)
66
+ GF_EXP[i] = GF_EXP[i - 255];
67
+ }
68
+ function gfMul(a, b) {
69
+ if (a === 0 || b === 0)
70
+ return 0;
71
+ return GF_EXP[GF_LOG[a] + GF_LOG[b]];
72
+ }
73
+ /** Reed-Solomon 生成多项式 g(x) = ∏(x − α^i),i=0..ecc-1。 */
74
+ function rsGeneratorPoly(ecc) {
75
+ const g = new Uint8Array(ecc + 1);
76
+ g[0] = 1;
77
+ for (let i = 0; i < ecc; i++) {
78
+ const next = new Uint8Array(g.length + 1);
79
+ for (let j = 0; j < g.length; j++) {
80
+ if (g[j] !== 0) {
81
+ next[j] ^= g[j];
82
+ next[j + 1] ^= gfMul(g[j], GF_EXP[i]);
83
+ }
84
+ }
85
+ g.set(next.subarray(0, ecc + 1));
86
+ }
87
+ return g;
88
+ }
89
+ /**
90
+ * 计算一块数据的 ECC 码字(多项式长除法,复刻 qrcode 库 mod 算法)。
91
+ * 生成多项式 g 高次在前(g[0] = x^ecc 系数);每步取被除数首项系数
92
+ * 消元 divisor 对齐项,直至被除数次数 < divisor 次数。
93
+ */
94
+ function rsComputeEcc(data, ecc) {
95
+ const g = rsGeneratorPoly(ecc);
96
+ // 被除数 = data || 0^ecc(多项式除法余数即 ECC)
97
+ let result = new Uint8Array(data.length + ecc);
98
+ result.set(data);
99
+ let offset = 0;
100
+ while (result.length - offset - g.length >= 0) {
101
+ const coeff = result[offset];
102
+ for (let i = 0; i < g.length; i++) {
103
+ result[offset + i] = (result[offset + i] ^ gfMul(g[i], coeff));
104
+ }
105
+ // 去掉前导零
106
+ while (offset < result.length && result[offset] === 0)
107
+ offset++;
108
+ }
109
+ // 余数(可能短于 ecc,前补零)
110
+ const rem = result.slice(offset);
111
+ const out = new Uint8Array(ecc);
112
+ out.set(rem, ecc - rem.length);
113
+ return out;
114
+ }
115
+ // ── 位流(Byte mode)──────────────────────────────────────────────────
116
+ function buildBitStream(bytes, version, dataCodewords) {
117
+ const bits = [];
118
+ const push = (value, len) => {
119
+ for (let i = len - 1; i >= 0; i--)
120
+ bits.push((value >> i) & 1);
121
+ };
122
+ push(0b0100, 4); // Byte mode
123
+ push(bytes.length, version <= 9 ? 8 : 16);
124
+ for (const b of bytes)
125
+ push(b, 8);
126
+ push(0, Math.min(4, dataCodewords * 8 - bits.length)); // terminator
127
+ while (bits.length % 8 !== 0)
128
+ bits.push(0); // 字节对齐
129
+ let pad = 0xec; // 填充 0xEC/0x11 交替
130
+ while (bits.length < dataCodewords * 8) {
131
+ push(pad, 8);
132
+ pad = pad === 0xec ? 0x11 : 0xec;
133
+ }
134
+ const out = new Uint8Array(dataCodewords);
135
+ for (let i = 0; i < dataCodewords; i++) {
136
+ let v = 0;
137
+ for (let b = 0; b < 8; b++)
138
+ v = (v << 1) | bits[i * 8 + b];
139
+ out[i] = v;
140
+ }
141
+ return out;
142
+ }
143
+ /** 块拆分 + ECC 计算 + 按块轮转交织。 */
144
+ function interleave(data, version) {
145
+ const blocks = BLOCKS_L[version - 1];
146
+ const totalData = TOTAL_CODEWORDS[version - 1] - ECC_PER_BLOCK_L[version - 1] * blocks;
147
+ const eccPerBlock = ECC_PER_BLOCK_L[version - 1];
148
+ const perBlock = totalData / blocks;
149
+ const blockData = [];
150
+ for (let b = 0; b < blocks; b++)
151
+ blockData.push(data.subarray(b * perBlock, (b + 1) * perBlock));
152
+ const eccBlocks = blockData.map((bd) => rsComputeEcc(bd, eccPerBlock));
153
+ const out = new Uint8Array(totalData + blocks * eccPerBlock);
154
+ let i = 0;
155
+ for (let r = 0; r < perBlock; r++)
156
+ for (let b = 0; b < blocks; b++)
157
+ out[i++] = blockData[b][r];
158
+ for (let r = 0; r < eccPerBlock; r++)
159
+ for (let b = 0; b < blocks; b++)
160
+ out[i++] = eccBlocks[b][r];
161
+ return out;
162
+ }
163
+ // ── 掩码与惩罚 ─────────────────────────────────────────────────────────
164
+ /** 掩码 0–7 判定(x, y 为模块坐标)。 */
165
+ export function maskPattern(mask, x, y) {
166
+ switch (mask) {
167
+ case 0:
168
+ return (x + y) % 2 === 0;
169
+ case 1:
170
+ return y % 2 === 0;
171
+ case 2:
172
+ return x % 3 === 0;
173
+ case 3:
174
+ return (x + y) % 3 === 0;
175
+ case 4:
176
+ return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0;
177
+ case 5:
178
+ return ((x * y) % 2) + ((x * y) % 3) === 0;
179
+ case 6:
180
+ return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
181
+ case 7:
182
+ return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
183
+ default:
184
+ return false;
185
+ }
186
+ }
187
+ /** 惩罚分(与 qrcode 库 mask-pattern.js 逐位一致:N1/N2/N3 滑动窗口/N4 ceil)。 */
188
+ function penaltyScore(m, size) {
189
+ const get = (y, x) => m[y * size + x];
190
+ let score = 0;
191
+ // N1: 行/列连续同色 ≥5,每多 1 个 +1(count=5→3, 6→4, ...)
192
+ for (let row = 0; row < size; row++) {
193
+ let sameCol = 0;
194
+ let sameRow = 0;
195
+ let lastCol = null;
196
+ let lastRow = null;
197
+ for (let col = 0; col < size; col++) {
198
+ const mc = get(row, col);
199
+ if (mc === lastCol)
200
+ sameCol++;
201
+ else {
202
+ if (sameCol >= 5)
203
+ score += 3 + (sameCol - 5);
204
+ lastCol = mc;
205
+ sameCol = 1;
206
+ }
207
+ const mr = get(col, row);
208
+ if (mr === lastRow)
209
+ sameRow++;
210
+ else {
211
+ if (sameRow >= 5)
212
+ score += 3 + (sameRow - 5);
213
+ lastRow = mr;
214
+ sameRow = 1;
215
+ }
216
+ }
217
+ if (sameCol >= 5)
218
+ score += 3 + (sameCol - 5);
219
+ if (sameRow >= 5)
220
+ score += 3 + (sameRow - 5);
221
+ }
222
+ // N2: 2×2 同色块
223
+ for (let row = 0; row < size - 1; row++) {
224
+ for (let col = 0; col < size - 1; col++) {
225
+ const last = get(row, col) + get(row, col + 1) + get(row + 1, col) + get(row + 1, col + 1);
226
+ if (last === 4 || last === 0)
227
+ score += 3;
228
+ }
229
+ }
230
+ // N3: 11 位滑动窗口匹配 0x5D0(10111010000)或 0x05D(00001011101)
231
+ for (let row = 0; row < size; row++) {
232
+ let bitsCol = 0;
233
+ let bitsRow = 0;
234
+ for (let col = 0; col < size; col++) {
235
+ bitsCol = ((bitsCol << 1) & 0x7ff) | get(row, col);
236
+ if (col >= 10 && (bitsCol === 0x5d0 || bitsCol === 0x05d))
237
+ score += 40;
238
+ bitsRow = ((bitsRow << 1) & 0x7ff) | get(col, row);
239
+ if (col >= 10 && (bitsRow === 0x5d0 || bitsRow === 0x05d))
240
+ score += 40;
241
+ }
242
+ }
243
+ // N4: 暗模块比例偏离 50%(ceil 到 5% 步进)
244
+ let dark = 0;
245
+ for (let i = 0; i < size * size; i++)
246
+ dark += m[i];
247
+ const k = Math.abs(Math.ceil((dark * 100) / (size * size) / 5) - 10);
248
+ score += k * 10;
249
+ return score;
250
+ }
251
+ // ── 格式/版本信息 ──────────────────────────────────────────────────────
252
+ /** 求最高有效位编号(qrcode 库 getBCHDigit 同款)。 */
253
+ function bchDigit(value) {
254
+ let digit = 0;
255
+ while (value !== 0) {
256
+ digit++;
257
+ value >>>= 1;
258
+ }
259
+ return digit;
260
+ }
261
+ /** BCH 除法(从实际最高位开始,避免高位置零导致错位)。 */
262
+ function bchRemainder(data, gen, genDigit) {
263
+ let d = data;
264
+ while (bchDigit(d) - genDigit >= 0) {
265
+ d ^= gen << (bchDigit(d) - genDigit);
266
+ }
267
+ return d;
268
+ }
269
+ /** BCH(15,5) 格式信息:data=EC(2b)|mask(3b),纠错后 XOR G15_MASK。 */
270
+ function formatBits(mask) {
271
+ const data = (0b01 << 3) | mask;
272
+ const gen = 0b10100110111;
273
+ const mask15 = 0b101010000010010;
274
+ const d = bchRemainder(data << 10, gen, bchDigit(gen));
275
+ return ((data << 10) | d) ^ mask15;
276
+ }
277
+ /** BCH(18,6) 版本信息(v7+)。 */
278
+ function versionBits(version) {
279
+ const gen = 0b1111100100101;
280
+ const d = bchRemainder(version << 12, gen, bchDigit(gen));
281
+ return (version << 12) | d;
282
+ }
283
+ function newMatrix(version) {
284
+ const size = version * 4 + 17;
285
+ return { size, m: new Uint8Array(size * size), reserved: new Uint8Array(size * size) };
286
+ }
287
+ function drawFinder(matrix, cx, cy) {
288
+ const { size, m, reserved } = matrix;
289
+ // 7×7 图案(±3)+ 分离带(±4);以 finder 中心 (cx, cy) 定位。
290
+ for (let dy = -4; dy <= 4; dy++) {
291
+ for (let dx = -4; dx <= 4; dx++) {
292
+ const x = cx + dx;
293
+ const y = cy + dy;
294
+ if (x < 0 || y < 0 || x >= size || y >= size)
295
+ continue;
296
+ reserved[y * size + x] = 1;
297
+ const inPattern = dx >= -3 && dx <= 3 && dy >= -3 && dy <= 3;
298
+ if (inPattern) {
299
+ const border = Math.abs(dx) === 3 || Math.abs(dy) === 3;
300
+ const inner = Math.abs(dx) <= 1 && Math.abs(dy) <= 1;
301
+ m[y * size + x] = border || inner ? 1 : 0;
302
+ }
303
+ else {
304
+ m[y * size + x] = 0; // 分离带
305
+ }
306
+ }
307
+ }
308
+ }
309
+ /** 放置功能图案并返回 (matrix, 数据位总长)。 */
310
+ function buildBaseMatrix(version, codewords) {
311
+ const matrix = newMatrix(version);
312
+ const { size, m, reserved } = matrix;
313
+ drawFinder(matrix, 3, 3);
314
+ drawFinder(matrix, size - 4, 3);
315
+ drawFinder(matrix, 3, size - 4);
316
+ // Timing(第 6 行/列)
317
+ for (let i = 8; i < size - 8; i++) {
318
+ const v = i % 2 === 0 ? 1 : 0;
319
+ reserved[6 * size + i] = 1;
320
+ m[6 * size + i] = v;
321
+ reserved[i * size + 6] = 1;
322
+ m[i * size + 6] = v;
323
+ }
324
+ // Alignment(跳过与 finder 重叠)
325
+ const centers = ALIGNMENT_PATTERNS[version - 1];
326
+ for (const cy of centers) {
327
+ for (const cx of centers) {
328
+ const overlaps = (cx <= 6 && cy <= 6) || (cx >= size - 7 && cy <= 6) || (cx <= 6 && cy >= size - 7);
329
+ if (overlaps)
330
+ continue;
331
+ for (let dy = -2; dy <= 2; dy++) {
332
+ for (let dx = -2; dx <= 2; dx++) {
333
+ const x = cx + dx;
334
+ const y = cy + dy;
335
+ reserved[y * size + x] = 1;
336
+ m[y * size + x] = Math.max(Math.abs(dx), Math.abs(dy)) !== 1 ? 1 : 0;
337
+ }
338
+ }
339
+ }
340
+ }
341
+ // Dark module (8, 4·version+9)
342
+ reserved[(version * 4 + 9) * size + 8] = 1;
343
+ m[(version * 4 + 9) * size + 8] = 1;
344
+ // Format info 区域(两处复制)先 reserve——数据 zigzag 不得占用
345
+ // (否则消耗数据位导致后续全部错位)。值在 applyMask 末尾写入。
346
+ // 坐标布局与 qrcode 库 setupFormatInfo 逐位一致(库参数序为 (row=y, col=x))。
347
+ const reserveFormat = (y, x) => {
348
+ if (x >= 0 && y >= 0 && x < size && y < size)
349
+ reserved[y * size + x] = 1;
350
+ };
351
+ for (let i = 0; i < 15; i++) {
352
+ // 垂直段(x=8 列)
353
+ if (i < 6)
354
+ reserveFormat(i, 8);
355
+ else if (i < 8)
356
+ reserveFormat(i + 1, 8);
357
+ else
358
+ reserveFormat(size - 15 + i, 8);
359
+ // 水平段(y=8 行)
360
+ if (i < 8)
361
+ reserveFormat(8, size - i - 1);
362
+ else if (i < 9)
363
+ reserveFormat(8, 15 - i - 1 + 1);
364
+ else
365
+ reserveFormat(8, 15 - i - 1);
366
+ }
367
+ reserveFormat(size - 8, 8); // 固定暗模块
368
+ // Version info 区域(v7+,两处 3×6 对称块;库参数序 (row=y, col=x))
369
+ if (version >= 7) {
370
+ for (let i = 0; i < 18; i++) {
371
+ const y = Math.floor(i / 3);
372
+ const x = (i % 3) + size - 11;
373
+ reserveFormat(y, x);
374
+ reserveFormat(x, y);
375
+ }
376
+ }
377
+ // 数据 zigzag 放置(与 qrcode 库 setupData 逐位一致):
378
+ // 从右下角起,每 2 列一组向上蛇行;跳过第 6 列;reserved 位跳过不消耗位;
379
+ // 数据耗尽后余位继续走(值保持 0,bitIndex 继续递减)。
380
+ let row = size - 1;
381
+ let inc = -1;
382
+ let bitIndex = 7;
383
+ let byteIndex = 0;
384
+ for (let col = size - 1; col > 0; col -= 2) {
385
+ if (col === 6)
386
+ col--;
387
+ for (;;) {
388
+ for (let c = 0; c < 2; c++) {
389
+ const x = col - c;
390
+ const y = row;
391
+ if (reserved[y * size + x] === 0) {
392
+ if (byteIndex < codewords.length) {
393
+ const byte = codewords[byteIndex];
394
+ m[y * size + x] = (byte >> bitIndex) & 1;
395
+ }
396
+ bitIndex--;
397
+ if (bitIndex === -1) {
398
+ byteIndex++;
399
+ bitIndex = 7;
400
+ }
401
+ }
402
+ }
403
+ row += inc;
404
+ if (row < 0 || size <= row) {
405
+ row -= inc;
406
+ inc = -inc;
407
+ break;
408
+ }
409
+ }
410
+ }
411
+ return matrix;
412
+ }
413
+ /** 对数据位应用掩码并绘制 format/version 信息。 */
414
+ function applyMask(matrix, version, mask) {
415
+ const { size, m, reserved } = matrix;
416
+ for (let y = 0; y < size; y++) {
417
+ for (let x = 0; x < size; x++) {
418
+ if (reserved[y * size + x] === 0 && maskPattern(mask, x, y)) {
419
+ m[y * size + x] = m[y * size + x] === 0 ? 1 : 0;
420
+ }
421
+ }
422
+ }
423
+ // Format info(两处复制,坐标布局与 qrcode 库 setupFormatInfo 一致,
424
+ // 库参数序为 (row=y, col=x))
425
+ const bits = formatBits(mask);
426
+ const setFormat = (y, x, bit) => {
427
+ m[y * size + x] = bit;
428
+ };
429
+ for (let i = 0; i < 15; i++) {
430
+ const mod = (bits >> i) & 1;
431
+ // 垂直段(x=8 列)
432
+ if (i < 6)
433
+ setFormat(i, 8, mod);
434
+ else if (i < 8)
435
+ setFormat(i + 1, 8, mod);
436
+ else
437
+ setFormat(size - 15 + i, 8, mod);
438
+ // 水平段(y=8 行)
439
+ if (i < 8)
440
+ setFormat(8, size - i - 1, mod);
441
+ else if (i < 9)
442
+ setFormat(8, 15 - i - 1 + 1, mod);
443
+ else
444
+ setFormat(8, 15 - i - 1, mod);
445
+ }
446
+ setFormat(size - 8, 8, 1); // 固定暗模块
447
+ // Version info(v7+,两处 3×6 对称块)
448
+ if (version >= 7) {
449
+ const vbits = versionBits(version);
450
+ for (let i = 0; i < 18; i++) {
451
+ const bit = (vbits >> i) & 1;
452
+ const y = Math.floor(i / 3);
453
+ const x = (i % 3) + size - 11;
454
+ setFormat(y, x, bit);
455
+ setFormat(x, y, bit);
456
+ }
457
+ }
458
+ }
459
+ /**
460
+ * 生成 QR 码矩阵(1 = 暗模块)。版本自动选择(1–10,纠错 L),
461
+ * 8 掩码全试 penalty 最低胜出。超长(>271 字节)抛 WeixinError("validation")。
462
+ */
463
+ export function encodeQrMatrix(payload) {
464
+ const bytes = new TextEncoder().encode(payload);
465
+ if (bytes.length === 0) {
466
+ throw new WeixinError("validation", "QR 载荷为空");
467
+ }
468
+ let version = 0;
469
+ for (let v = 1; v <= 10; v++) {
470
+ if (bytes.length <= CAPACITY_BYTE_L[v - 1]) {
471
+ version = v;
472
+ break;
473
+ }
474
+ }
475
+ if (version === 0) {
476
+ throw new WeixinError("validation", `QR 载荷过长(${bytes.length} 字节,上限 271)`);
477
+ }
478
+ const dataCodewords = TOTAL_CODEWORDS[version - 1] - ECC_PER_BLOCK_L[version - 1] * BLOCKS_L[version - 1];
479
+ const bitStream = buildBitStream(bytes, version, dataCodewords);
480
+ const codewords = interleave(bitStream, version);
481
+ // 8 掩码全试:penalty 最低者胜出
482
+ let best = null;
483
+ let bestScore = Infinity;
484
+ for (let mask = 0; mask < 8; mask++) {
485
+ const candidate = buildBaseMatrix(version, codewords);
486
+ applyMask(candidate, version, mask);
487
+ const score = penaltyScore(candidate.m, candidate.size);
488
+ if (score < bestScore) {
489
+ bestScore = score;
490
+ best = candidate;
491
+ }
492
+ }
493
+ return best.m;
494
+ }
495
+ /** 终端渲染:半块字符(00→" "、01→"▀"、10→"▄"、11→"█"),静区 4。 */
496
+ export function renderQrMatrix(matrix, size, scale = 1) {
497
+ const quiet = 4;
498
+ const rows = [];
499
+ // 顶静区(偶数行对齐:静区行全白)
500
+ for (let r = 0; r < quiet / 2; r++)
501
+ rows.push(" ".repeat((size + quiet * 2) * scale));
502
+ for (let y = 0; y < size; y += 2) {
503
+ let line = " ".repeat(quiet * scale);
504
+ for (let x = 0; x < size; x++) {
505
+ const top = y < size ? matrix[y * size + x] : 0;
506
+ const bottom = y + 1 < size ? matrix[(y + 1) * size + x] : 0;
507
+ const pair = (top << 1) | bottom;
508
+ const ch = pair === 0 ? " " : pair === 1 ? "▀" : pair === 2 ? "▄" : "█";
509
+ line += ch.repeat(scale);
510
+ }
511
+ line += " ".repeat(quiet * scale);
512
+ rows.push(line);
513
+ }
514
+ // 底静区
515
+ for (let r = 0; r < quiet / 2; r++)
516
+ rows.push(" ".repeat((size + quiet * 2) * scale));
517
+ return rows;
518
+ }
519
+ /** 便捷入口:payload → 渲染行(静区已含,可直接 stdout)。 */
520
+ export function renderQr(payload, scale = 1) {
521
+ const m = encodeQrMatrix(payload);
522
+ const size = Math.sqrt(m.length);
523
+ return renderQrMatrix(m, size, scale);
524
+ }
525
+ //# sourceMappingURL=qr.js.map