@planet-matrix/mobius-model 0.6.0 → 0.9.0

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 (233) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/index.js +706 -36
  3. package/dist/index.js.map +855 -59
  4. package/package.json +28 -16
  5. package/src/ai/README.md +1 -0
  6. package/src/ai/ai.ts +107 -0
  7. package/src/ai/chat-completion-ai/aihubmix-chat-completion.ts +78 -0
  8. package/src/ai/chat-completion-ai/chat-completion-ai.ts +270 -0
  9. package/src/ai/chat-completion-ai/chat-completion.ts +189 -0
  10. package/src/ai/chat-completion-ai/index.ts +7 -0
  11. package/src/ai/chat-completion-ai/lingyiwanwu-chat-completion.ts +78 -0
  12. package/src/ai/chat-completion-ai/ohmygpt-chat-completion.ts +78 -0
  13. package/src/ai/chat-completion-ai/openai-next-chat-completion.ts +78 -0
  14. package/src/ai/embedding-ai/embedding-ai.ts +63 -0
  15. package/src/ai/embedding-ai/embedding.ts +50 -0
  16. package/src/ai/embedding-ai/index.ts +4 -0
  17. package/src/ai/embedding-ai/openai-next-embedding.ts +23 -0
  18. package/src/ai/index.ts +4 -0
  19. package/src/aio/README.md +100 -0
  20. package/src/aio/content.ts +141 -0
  21. package/src/aio/index.ts +3 -0
  22. package/src/aio/json.ts +127 -0
  23. package/src/aio/prompt.ts +246 -0
  24. package/src/basic/README.md +20 -15
  25. package/src/basic/error.ts +19 -5
  26. package/src/basic/function.ts +2 -2
  27. package/src/basic/index.ts +1 -0
  28. package/src/basic/schedule.ts +111 -0
  29. package/src/basic/stream.ts +135 -25
  30. package/src/credential/README.md +107 -0
  31. package/src/credential/api-key.ts +158 -0
  32. package/src/credential/bearer.ts +73 -0
  33. package/src/credential/index.ts +4 -0
  34. package/src/credential/json-web-token.ts +96 -0
  35. package/src/credential/password.ts +170 -0
  36. package/src/cron/README.md +86 -0
  37. package/src/cron/cron.ts +87 -0
  38. package/src/cron/index.ts +1 -0
  39. package/src/drizzle/README.md +1 -0
  40. package/src/drizzle/drizzle.ts +1 -0
  41. package/src/drizzle/helper.ts +47 -0
  42. package/src/drizzle/index.ts +5 -0
  43. package/src/drizzle/infer.ts +52 -0
  44. package/src/drizzle/kysely.ts +8 -0
  45. package/src/drizzle/pagination.ts +200 -0
  46. package/src/email/README.md +1 -0
  47. package/src/email/index.ts +1 -0
  48. package/src/email/resend.ts +25 -0
  49. package/src/event/class-event-proxy.ts +6 -5
  50. package/src/event/common.ts +13 -3
  51. package/src/event/event-manager.ts +3 -3
  52. package/src/event/instance-event-proxy.ts +6 -5
  53. package/src/event/internal.ts +4 -4
  54. package/src/form/README.md +25 -0
  55. package/src/form/index.ts +1 -0
  56. package/src/form/inputor-controller/base.ts +874 -0
  57. package/src/form/inputor-controller/boolean.ts +39 -0
  58. package/src/form/inputor-controller/file.ts +39 -0
  59. package/src/form/inputor-controller/form.ts +181 -0
  60. package/src/form/inputor-controller/helper.ts +117 -0
  61. package/src/form/inputor-controller/index.ts +17 -0
  62. package/src/form/inputor-controller/multi-select.ts +99 -0
  63. package/src/form/inputor-controller/number.ts +116 -0
  64. package/src/form/inputor-controller/select.ts +109 -0
  65. package/src/form/inputor-controller/text.ts +82 -0
  66. package/src/http/READMD.md +1 -0
  67. package/src/http/api/api-core.ts +84 -0
  68. package/src/http/api/api-handler.ts +79 -0
  69. package/src/http/api/api-host.ts +47 -0
  70. package/src/http/api/api-result.ts +56 -0
  71. package/src/http/api/api-schema.ts +154 -0
  72. package/src/http/api/api-server.ts +130 -0
  73. package/src/http/api/api-test.ts +142 -0
  74. package/src/http/api/api-type.ts +37 -0
  75. package/src/http/api/api.ts +81 -0
  76. package/src/http/api/index.ts +11 -0
  77. package/src/http/api-adapter/api-core-node-http.ts +260 -0
  78. package/src/http/api-adapter/api-host-node-http.ts +156 -0
  79. package/src/http/api-adapter/api-result-arktype.ts +297 -0
  80. package/src/http/api-adapter/api-result-zod.ts +286 -0
  81. package/src/http/api-adapter/index.ts +5 -0
  82. package/src/http/bin/gen-api-list/gen-api-list.ts +126 -0
  83. package/src/http/bin/gen-api-list/index.ts +1 -0
  84. package/src/http/bin/gen-api-test/gen-api-test.ts +136 -0
  85. package/src/http/bin/gen-api-test/index.ts +1 -0
  86. package/src/http/bin/gen-api-type/calc-code.ts +25 -0
  87. package/src/http/bin/gen-api-type/gen-api-type.ts +127 -0
  88. package/src/http/bin/gen-api-type/index.ts +2 -0
  89. package/src/http/bin/index.ts +2 -0
  90. package/src/http/index.ts +3 -0
  91. package/src/huawei/README.md +1 -0
  92. package/src/huawei/index.ts +2 -0
  93. package/src/huawei/moderation/index.ts +1 -0
  94. package/src/huawei/moderation/moderation.ts +355 -0
  95. package/src/huawei/obs/esdk-obs-nodejs.d.ts +87 -0
  96. package/src/huawei/obs/index.ts +1 -0
  97. package/src/huawei/obs/obs.ts +42 -0
  98. package/src/index.ts +19 -2
  99. package/src/json/README.md +92 -0
  100. package/src/json/index.ts +1 -0
  101. package/src/json/repair.ts +18 -0
  102. package/src/log/logger.ts +15 -4
  103. package/src/openai/README.md +1 -0
  104. package/src/openai/index.ts +1 -0
  105. package/src/openai/openai.ts +510 -0
  106. package/src/orchestration/README.md +9 -7
  107. package/src/orchestration/dispatching/dispatcher.ts +83 -0
  108. package/src/orchestration/dispatching/index.ts +2 -0
  109. package/src/orchestration/dispatching/selector/base-selector.ts +39 -0
  110. package/src/orchestration/dispatching/selector/down-count-selector.ts +119 -0
  111. package/src/orchestration/dispatching/selector/index.ts +2 -0
  112. package/src/orchestration/index.ts +2 -0
  113. package/src/orchestration/scheduling/index.ts +2 -0
  114. package/src/orchestration/scheduling/scheduler.ts +103 -0
  115. package/src/orchestration/scheduling/task.ts +32 -0
  116. package/src/random/README.md +8 -7
  117. package/src/random/base.ts +66 -0
  118. package/src/random/index.ts +5 -1
  119. package/src/random/random-boolean.ts +40 -0
  120. package/src/random/random-integer.ts +60 -0
  121. package/src/random/random-number.ts +72 -0
  122. package/src/random/random-string.ts +66 -0
  123. package/src/request/README.md +108 -0
  124. package/src/request/fetch/base.ts +108 -0
  125. package/src/request/fetch/browser.ts +285 -0
  126. package/src/request/fetch/general.ts +20 -0
  127. package/src/request/fetch/index.ts +4 -0
  128. package/src/request/fetch/nodejs.ts +285 -0
  129. package/src/request/index.ts +2 -0
  130. package/src/request/request/base.ts +250 -0
  131. package/src/request/request/general.ts +64 -0
  132. package/src/request/request/index.ts +3 -0
  133. package/src/request/request/resource.ts +68 -0
  134. package/src/result/README.md +4 -0
  135. package/src/result/controller.ts +54 -0
  136. package/src/result/either.ts +193 -0
  137. package/src/result/index.ts +2 -0
  138. package/src/route/README.md +105 -0
  139. package/src/route/adapter/browser.ts +122 -0
  140. package/src/route/adapter/driver.ts +56 -0
  141. package/src/route/adapter/index.ts +2 -0
  142. package/src/route/index.ts +3 -0
  143. package/src/route/router/index.ts +2 -0
  144. package/src/route/router/route.ts +630 -0
  145. package/src/route/router/router.ts +1642 -0
  146. package/src/route/uri/hash.ts +308 -0
  147. package/src/route/uri/index.ts +7 -0
  148. package/src/route/uri/pathname.ts +376 -0
  149. package/src/route/uri/search.ts +413 -0
  150. package/src/socket/README.md +105 -0
  151. package/src/socket/client/index.ts +2 -0
  152. package/src/socket/client/socket-unit.ts +660 -0
  153. package/src/socket/client/socket.ts +203 -0
  154. package/src/socket/common/index.ts +2 -0
  155. package/src/socket/common/socket-unit-common.ts +23 -0
  156. package/src/socket/common/socket-unit-heartbeat.ts +427 -0
  157. package/src/socket/index.ts +3 -0
  158. package/src/socket/server/index.ts +3 -0
  159. package/src/socket/server/server.ts +183 -0
  160. package/src/socket/server/socket-unit.ts +449 -0
  161. package/src/socket/server/socket.ts +264 -0
  162. package/src/storage/table.ts +3 -3
  163. package/src/timer/expiration/expiration-manager.ts +3 -3
  164. package/src/timer/expiration/remaining-manager.ts +3 -3
  165. package/src/tube/README.md +99 -0
  166. package/src/tube/helper.ts +138 -0
  167. package/src/tube/index.ts +2 -0
  168. package/src/tube/tube.ts +880 -0
  169. package/src/weixin/README.md +1 -0
  170. package/src/weixin/index.ts +2 -0
  171. package/src/weixin/official-account/authorization.ts +159 -0
  172. package/src/weixin/official-account/index.ts +2 -0
  173. package/src/weixin/official-account/js-api.ts +134 -0
  174. package/src/weixin/open/index.ts +1 -0
  175. package/src/weixin/open/oauth2.ts +133 -0
  176. package/tests/unit/ai/ai.spec.ts +85 -0
  177. package/tests/unit/aio/content.spec.ts +105 -0
  178. package/tests/unit/aio/json.spec.ts +147 -0
  179. package/tests/unit/aio/prompt.spec.ts +111 -0
  180. package/tests/unit/basic/error.spec.ts +16 -4
  181. package/tests/unit/basic/schedule.spec.ts +74 -0
  182. package/tests/unit/basic/stream.spec.ts +90 -37
  183. package/tests/unit/credential/api-key.spec.ts +37 -0
  184. package/tests/unit/credential/bearer.spec.ts +23 -0
  185. package/tests/unit/credential/json-web-token.spec.ts +23 -0
  186. package/tests/unit/credential/password.spec.ts +41 -0
  187. package/tests/unit/cron/cron.spec.ts +84 -0
  188. package/tests/unit/event/class-event-proxy.spec.ts +3 -3
  189. package/tests/unit/event/event-manager.spec.ts +3 -3
  190. package/tests/unit/event/instance-event-proxy.spec.ts +3 -3
  191. package/tests/unit/form/inputor-controller/base.spec.ts +458 -0
  192. package/tests/unit/form/inputor-controller/boolean.spec.ts +30 -0
  193. package/tests/unit/form/inputor-controller/file.spec.ts +27 -0
  194. package/tests/unit/form/inputor-controller/form.spec.ts +120 -0
  195. package/tests/unit/form/inputor-controller/helper.spec.ts +67 -0
  196. package/tests/unit/form/inputor-controller/multi-select.spec.ts +34 -0
  197. package/tests/unit/form/inputor-controller/number.spec.ts +36 -0
  198. package/tests/unit/form/inputor-controller/select.spec.ts +49 -0
  199. package/tests/unit/form/inputor-controller/text.spec.ts +34 -0
  200. package/tests/unit/http/api/api-core-host.spec.ts +207 -0
  201. package/tests/unit/http/api/api-schema.spec.ts +120 -0
  202. package/tests/unit/http/api/api-server.spec.ts +363 -0
  203. package/tests/unit/http/api/api-test.spec.ts +117 -0
  204. package/tests/unit/http/api/api.spec.ts +121 -0
  205. package/tests/unit/http/api-adapter/node-http.spec.ts +191 -0
  206. package/tests/unit/json/repair.spec.ts +11 -0
  207. package/tests/unit/log/logger.spec.ts +19 -4
  208. package/tests/unit/openai/openai.spec.ts +64 -0
  209. package/tests/unit/orchestration/dispatching/dispatcher.spec.ts +41 -0
  210. package/tests/unit/orchestration/dispatching/selector/down-count-selector.spec.ts +81 -0
  211. package/tests/unit/orchestration/scheduling/scheduler.spec.ts +103 -0
  212. package/tests/unit/random/base.spec.ts +58 -0
  213. package/tests/unit/random/random-boolean.spec.ts +25 -0
  214. package/tests/unit/random/random-integer.spec.ts +32 -0
  215. package/tests/unit/random/random-number.spec.ts +33 -0
  216. package/tests/unit/random/random-string.spec.ts +22 -0
  217. package/tests/unit/request/fetch/browser.spec.ts +222 -0
  218. package/tests/unit/request/fetch/general.spec.ts +43 -0
  219. package/tests/unit/request/fetch/nodejs.spec.ts +225 -0
  220. package/tests/unit/request/request/base.spec.ts +385 -0
  221. package/tests/unit/request/request/general.spec.ts +161 -0
  222. package/tests/unit/route/router/route.spec.ts +431 -0
  223. package/tests/unit/route/router/router.spec.ts +407 -0
  224. package/tests/unit/route/uri/hash.spec.ts +72 -0
  225. package/tests/unit/route/uri/pathname.spec.ts +147 -0
  226. package/tests/unit/route/uri/search.spec.ts +107 -0
  227. package/tests/unit/socket/client.spec.ts +208 -0
  228. package/tests/unit/socket/server.spec.ts +135 -0
  229. package/tests/unit/socket/socket-unit-heartbeat.spec.ts +214 -0
  230. package/tests/unit/tube/helper.spec.ts +139 -0
  231. package/tests/unit/tube/tube.spec.ts +501 -0
  232. package/src/random/string.ts +0 -35
  233. package/tests/unit/random/string.spec.ts +0 -11
@@ -0,0 +1,92 @@
1
+ # JSON
2
+
3
+ ## Description
4
+
5
+ JSON 模块提供围绕 JSON(JavaScript Object Notation)这一数据表示格式的相关基础能力,用于承载与 JSON 本身直接相关、且值得长期稳定维护的公共语义。
6
+
7
+ 当前模块公开能力还很少,暂时只有 `repair` 这一项文本修复入口;但这不意味着模块边界仅限于修复本身,而是说明 JSON 相关能力仍在逐步沉淀中。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 JSON 模块时,应把它看作一个围绕 JSON 本身建立的问题域模块,而不是一个泛化的数据清洗工具箱,也不是任意对象处理逻辑的收纳处。判断某项能力是否应进入这里,关键不在于它“碰巧输入或输出了 JSON”,而在于它是否直接服务于 JSON 这一表示格式本身的稳定语义。
12
+
13
+ 这类能力通常可能包括 JSON 文本修复、序列化与反序列化辅助、稳定格式化、合法性判断、边界明确的表示转换,或其它直接围绕 JSON 文本与 JSON 语义展开的基础能力。当前仓库里之所以只有 `repair`,只是因为模块还处在较早阶段,而不是因为其它 JSON 相关能力天然不属于这里。
14
+
15
+ 当前边界可以概括为:
16
+
17
+ - 它可以承载与 JSON 本身直接相关的公共能力,而不局限于当前已经存在的 `repair`。
18
+ - 它关注的是 JSON 作为表示格式时的稳定语义,而不是解析后对象在业务上的含义。
19
+ - 它可以处理文本层问题,也可以在未来扩展到仍然属于 JSON 语义边界内的其它能力。
20
+ - 它不应因为名称宽泛就退化成杂项对象工具箱或通用数据清洗工具箱。
21
+
22
+ 因此,这个模块不应承载对象结构校验、字段语义补全、领域模型迁移、容错合并或业务级默认值推导。那些问题可能发生在 JSON 之前或之后,也可能依赖 JSON 作为载体,但并不因此自动属于 JSON 模块。
23
+
24
+ ## For Using
25
+
26
+ 当你需要复用一类与 JSON 本身直接相关的基础能力,而不是在业务代码里零散处理 JSON 文本、解析入口或表示细节时,可以使用这个模块。当前最直接的使用场景,是在解析之前先修复接近 JSON、但带有轻微格式噪声的文本,例如人工编辑配置、LLM 输出、宽松的第三方接口返回、日志片段复制结果,或历史遗留系统吐出的近似 JSON 文本。
27
+
28
+ 当前公共能力可以从使用意图上理解为一类入口:
29
+
30
+ - 文本修复入口:把接近 JSON 的字符串整理成可继续交给标准解析器处理的 JSON 文本。
31
+
32
+ 随着模块扩展,未来也可以继续容纳其它直接服务于 JSON 语义的公共入口。但即便如此,使用时仍应区分“JSON 本身的问题”和“业务对象的问题”:如果你的问题已经变成对象级校验、字段默认值注入、结构迁移或业务容错,那么应在其它模块或上层流程中继续分层,而不要把这些职责压回 JSON 模块。
33
+
34
+ ## For Contributing
35
+
36
+ 贡献 JSON 模块时,应优先判断新增能力是否直接表达 JSON 本身的稳定语义,而不是借着 JSON 的名字引入对象级处理、业务级清洗或协议级解释逻辑。这个模块可以继续增长,但增长方向应围绕 JSON 自身的问题域展开,而不是因为名字宽泛就无限吸纳一切与对象或文本有关的工具。
37
+
38
+ 在扩展时,应优先遵守以下边界:
39
+
40
+ - 公共能力应直接围绕 JSON 本身展开,例如表示修复、合法性处理、稳定序列化或其它明确属于 JSON 语义的能力。
41
+ - 不要把 schema 校验、字段补全、结构迁移、业务默认值或容错合并直接写成 JSON 模块的公共职责。
42
+ - 若新增能力只是“使用了 JSON”但核心问题并不属于 JSON 本身,则应放在更合适的模块,而不是为了归类方便塞进这里。
43
+ - 对于像 `repair` 这类带有容错性质的能力,应保持失败语义清楚;当输入不能被稳定处理时,抛错通常比返回含糊结果更符合模块边界。
44
+
45
+ ### JSDoc 注释格式要求
46
+
47
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
48
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
49
+ - 如果描述后还有其他内容,应在描述后加一个空行。
50
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
51
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
52
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
53
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
54
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
55
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
56
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
57
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
58
+
59
+ ### 实现规范要求
60
+
61
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
62
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
63
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
64
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
65
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
66
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
67
+ - 辅助元素永远不要公开导出。
68
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/json/internal.ts`;若当前模块仍然很小,则不必为了形式拆出没有稳定价值的共享文件。
69
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
70
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
71
+ - 子模块不需要有自己的 `README.md`。
72
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
73
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
74
+ - 实现时应优先保持输入、输出与失败条件的语义清楚;若某项能力具有容错或修复性质,不应为了支持更多特例不断堆叠难以解释的隐式规则。
75
+
76
+ ### 导出策略要求
77
+
78
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
79
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
80
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
81
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的 JSON 模块语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
82
+
83
+ ### 测试要求
84
+
85
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
86
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
87
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
88
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
89
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
90
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
91
+ - 模块的单元测试文件目录是 `./tests/unit/json`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/json/<sub-module-name>`。
92
+ - 测试应覆盖各公共能力各自的稳定语义。对于当前已有的 `repair`,重点覆盖可稳定修复的常见格式问题,以及超出修复边界时是否以明确失败结束,而不是返回含糊结果。
@@ -0,0 +1 @@
1
+ export * from "./repair.ts"
@@ -0,0 +1,18 @@
1
+ import { jsonrepair } from "jsonrepair"
2
+
3
+ /**
4
+ * 修复常见格式问题后的 JSON 文本字符串。
5
+ *
6
+ * 该函数面向“文本表示修复”这一问题:当输入接近 JSON、但包含单引号、未加引号的键名、尾随逗号等常见格式问题时,可以先把它整理成合法 JSON 文本,再交给后续解析流程。
7
+ *
8
+ * @example
9
+ * ```
10
+ * // Expect: "{\"foo\":1}"
11
+ * const example1 = repairJson("{foo:1,}")
12
+ * // Expect: "{\"name\":\"mobius\"}"
13
+ * const example2 = repairJson("{'name':'mobius',}")
14
+ * ```
15
+ */
16
+ export const repairJson = (text: string): string => {
17
+ return jsonrepair(text)
18
+ }
package/src/log/logger.ts CHANGED
@@ -135,7 +135,8 @@ export class Logger implements Disposable {
135
135
  return logger
136
136
  }
137
137
 
138
- protected name: string
138
+ protected defaultName: string
139
+ protected name?: string | undefined
139
140
  protected parent?: Logger | undefined
140
141
  protected childs: Logger[]
141
142
  protected parentConfigs?: LoggerConfigs | undefined
@@ -149,7 +150,8 @@ export class Logger implements Disposable {
149
150
  private cachedLogMethodMap: Map<string, (...messages: unknown[]) => this>
150
151
 
151
152
  constructor(options: LoggerOptions) {
152
- this.name = options.name ?? "Unnamed"
153
+ this.defaultName = "Unnamed"
154
+ this.name = options.name
153
155
  this.parent = options.parent
154
156
  this.childs = []
155
157
  const parentConfigs = options.parent?.getConfigs()
@@ -170,17 +172,26 @@ export class Logger implements Disposable {
170
172
  this.cachedLogMethodMap = new Map()
171
173
  }
172
174
 
175
+ /**
176
+ * Set the default name if the current name is empty,
177
+ * and return the logger for chaining.
178
+ */
179
+ setDefaultName(name: string): this {
180
+ this.defaultName = name
181
+ return this
182
+ }
183
+
173
184
  /**
174
185
  * Get the logger display name.
175
186
  */
176
187
  getName(): string {
177
- return this.name
188
+ return this.name ?? this.defaultName
178
189
  }
179
190
 
180
191
  /**
181
192
  * Set the logger display name.
182
193
  */
183
- setName(name: string): this {
194
+ setName(name: string | undefined): this {
184
195
  this.name = name
185
196
  return this
186
197
  }
@@ -0,0 +1 @@
1
+ # Openai
@@ -0,0 +1 @@
1
+ export * from "./openai.ts"
@@ -0,0 +1,510 @@
1
+ import type { ClientOptions } from "openai"
2
+ import { OpenAI } from "openai"
3
+ import type { Stream } from "openai/streaming"
4
+ import { Agent, setGlobalDispatcher } from "undici"
5
+
6
+ import type { LoggerFriendly, LoggerFriendlyOptions } from "#Source/log/index.ts"
7
+ import type { WithAbortSignal } from "#Source/abort/index.ts"
8
+ import type { Either } from "#Source/result/index.ts"
9
+
10
+ import { Logger } from "#Source/log/index.ts"
11
+ import * as Aio from "#Source/aio/index.ts"
12
+ import { controllerFromEitherType } from "#Source/result/index.ts"
13
+ import { Tube } from "#Source/tube/index.ts"
14
+ import { scheduleMacroTask, streamConsumeInMacroTask, streamTransformInMacroTask } from "#Source/basic/index.ts"
15
+
16
+ export { OpenAI } from "openai"
17
+ export { Stream } from "openai/streaming"
18
+
19
+ export interface WithOpenai {
20
+ openai: Openai
21
+ }
22
+
23
+ export interface OriginalChatCompletionOptions
24
+ extends Omit<OpenAI.Chat.ChatCompletionCreateParams, never>, WithAbortSignal {
25
+ }
26
+ export type OriginalChatCompletionResult = OpenAI.Chat.ChatCompletion
27
+ export type OriginalChatCompletionChunkResult = OpenAI.Chat.Completions.ChatCompletionChunk
28
+
29
+ export interface CustomChatCompletionOptions extends OriginalChatCompletionOptions {
30
+ /**
31
+ * In milliseconds.
32
+ */
33
+ timeout?: number | undefined
34
+ }
35
+ export interface CustomChatCompletionNonStreamingLeft {
36
+ code: "NO_CHOICE" | "TOO_LONG" | "FILTERED" | "REFUSED" | "EMPTY" | "UNKNOWN"
37
+ }
38
+ export interface CustomChatCompletionNonStreamingRight {
39
+ content: string
40
+ token: number
41
+ }
42
+ export type CustomChatCompletionNonStreamingResult
43
+ = Either<CustomChatCompletionNonStreamingLeft, CustomChatCompletionNonStreamingRight>
44
+
45
+ export interface CustomChatCompletionStreamingLeft {
46
+ code: never
47
+ }
48
+ export interface Completion {
49
+ content: Aio.TextContent
50
+ token: Aio.NumberContent
51
+ }
52
+ export interface CustomChatCompletionStreamingRight {
53
+ completionTube: Tube<Completion>
54
+ }
55
+ export type CustomChatCompletionStreamingResult =
56
+ Either<CustomChatCompletionStreamingLeft, CustomChatCompletionStreamingRight>
57
+
58
+ export interface OriginalEmbeddingOptions
59
+ extends OpenAI.Embeddings.EmbeddingCreateParams, WithAbortSignal {
60
+ }
61
+ export type OriginalEmbeddingResult = OpenAI.Embeddings.CreateEmbeddingResponse
62
+
63
+ export interface CustomEmbeddingOptions extends WithAbortSignal {
64
+ input: string
65
+ model?: OpenAI.Embeddings.EmbeddingCreateParams["model"] | undefined
66
+ dimensions?: OpenAI.Embeddings.EmbeddingCreateParams["dimensions"] | undefined
67
+ }
68
+ export interface CustomEmbeddingResultLeft {
69
+ code: "NO_EMBEDDING" | "MODEL_MISMATCH" | "UNKNOWN"
70
+ }
71
+ export interface CustomEmbeddingResultRight {
72
+ embedding: number[]
73
+ usage: {
74
+ promptTokens: number
75
+ totalTokens: number
76
+ }
77
+ }
78
+ export type CustomEmbeddingResult =
79
+ Either<CustomEmbeddingResultLeft, CustomEmbeddingResultRight>
80
+
81
+ export type CustomSpeechOptions = Omit<OpenAI.Audio.SpeechCreateParams, "input" | "model" | "voice" | "response_format"> & {
82
+ input: string
83
+ }
84
+
85
+ export interface OpenaiOptions extends LoggerFriendlyOptions {
86
+ apiKey: Exclude<ClientOptions["apiKey"], undefined | null>
87
+ baseUrl: Exclude<ClientOptions["baseURL"], undefined | null>
88
+ /**
89
+ * In milliseconds.
90
+ *
91
+ * @default 5 * 60 * 1000
92
+ */
93
+ timeout?: ClientOptions["timeout"] | undefined
94
+ }
95
+ interface ResolvedOpenaiOptions {
96
+ apiKey: Exclude<ClientOptions["apiKey"], undefined | null>
97
+ baseUrl: Exclude<ClientOptions["baseURL"], undefined | null>
98
+ timeout: Exclude<ClientOptions["timeout"], undefined | null>
99
+ }
100
+ export class Openai implements LoggerFriendly {
101
+ protected options: ResolvedOpenaiOptions
102
+
103
+ readonly logger: Logger
104
+ protected openai: OpenAI
105
+
106
+ constructor(options: OpenaiOptions) {
107
+ this.options = {
108
+ apiKey: options.apiKey,
109
+ baseUrl: options.baseUrl,
110
+ timeout: options.timeout ?? 5 * 60 * 1_000,
111
+ }
112
+
113
+ this.logger = Logger.fromOptions(options).setDefaultName("Openai")
114
+ this.openai = new OpenAI({
115
+ baseURL: this.options.baseUrl,
116
+ // NOTE: 若服务方证书出现问题可暂时开启这个选项,问题解决后应该第一时间关闭。
117
+ // fetch: this.customFetchThatIgnoreTheCAs,
118
+ timeout: this.options.timeout,
119
+ apiKey: this.options.apiKey,
120
+ })
121
+ }
122
+
123
+ protected async customFetchThatIgnoreTheCAs(
124
+ url: string, init?: RequestInit
125
+ ): Promise<Response> {
126
+ setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } }))
127
+
128
+ const response = await fetch(url, init)
129
+ if (response.ok === false) {
130
+ throw new Error(`Request failed with status ${response.status}`)
131
+ }
132
+
133
+ return response
134
+ }
135
+
136
+ getBaseUrl(): string {
137
+ return this.openai.baseURL
138
+ }
139
+
140
+ /**
141
+ * 原始 chatCompletion 方法的包装,但将其固定为非流式且不自动重试。
142
+ */
143
+ async originalChatCompletionNonStreaming(
144
+ options: OriginalChatCompletionOptions,
145
+ ): Promise<OriginalChatCompletionResult> {
146
+ try {
147
+ const { abortSignal, ...restOptions } = options
148
+ const response = await this.openai.chat.completions.create({
149
+ ...restOptions,
150
+ stream: false,
151
+ }, {
152
+ maxRetries: 0,
153
+ signal: abortSignal,
154
+ })
155
+ return response
156
+ }
157
+ catch (exception) {
158
+ this.logger.error("Error creating chat completion: ", exception)
159
+ throw exception
160
+ }
161
+ }
162
+
163
+ /**
164
+ * 原始 chatCompletion 方法的包装,但将其固定为流式且不自动重试。
165
+ */
166
+ async originalChatCompletionStreaming(
167
+ options: OriginalChatCompletionOptions,
168
+ ): Promise<Stream<OriginalChatCompletionChunkResult>> {
169
+ try {
170
+ const { abortSignal, ...restOptions } = options
171
+ const response = await this.openai.chat.completions.create({
172
+ ...restOptions,
173
+ stream: true,
174
+ }, {
175
+ maxRetries: 0,
176
+ signal: abortSignal,
177
+ })
178
+ return response
179
+ }
180
+ catch (exception) {
181
+ this.logger.error("Error creating chat completion: ", exception)
182
+ throw exception
183
+ }
184
+ }
185
+
186
+ /**
187
+ * 此方法不会自动重试,调用方需要通过错误信息自行处理。
188
+ *
189
+ * {@link https://platform.openai.com/docs/api-reference/chat | Chat - OpenAI API}
190
+ */
191
+ async customChatCompletionNonStreaming(
192
+ options: CustomChatCompletionOptions,
193
+ ): Promise<CustomChatCompletionNonStreamingResult> {
194
+ const controller = controllerFromEitherType<CustomChatCompletionNonStreamingResult>()
195
+ try {
196
+ const { abortSignal, ...restOptions } = options
197
+ const response = await this.openai.chat.completions.create({
198
+ ...restOptions,
199
+ stream: false,
200
+ }, {
201
+ maxRetries: 0,
202
+ signal: abortSignal,
203
+ })
204
+ const choice = response.choices[0]
205
+ if (choice === undefined) {
206
+ this.logger.error("Chat completion is empty.")
207
+ return await controller.returnLeft({ code: "NO_CHOICE" })
208
+ }
209
+
210
+ const finishReason = choice.finish_reason
211
+ if (finishReason === "length") {
212
+ this.logger.error("Chat completion is too long.")
213
+ return await controller.returnLeft({ code: "TOO_LONG" })
214
+ }
215
+ if (finishReason === "content_filter") {
216
+ this.logger.error("Chat completion is filtered.")
217
+ return await controller.returnLeft({ code: "FILTERED" })
218
+ }
219
+
220
+ const refusal = choice.message.refusal
221
+ if (refusal !== null && refusal !== undefined) {
222
+ this.logger.error(`Chat completion is refused: ${refusal}`)
223
+ return await controller.returnLeft({ code: "REFUSED" })
224
+ }
225
+ const content = choice.message.content
226
+ if (content === null) {
227
+ this.logger.error("Chat completion is empty.")
228
+ return await controller.returnLeft({ code: "EMPTY" })
229
+ }
230
+
231
+ return await controller.returnRight({
232
+ content,
233
+ // TODO: count the tokens
234
+ token: 0,
235
+ })
236
+ }
237
+ catch (exception) {
238
+ this.logger.error("Error creating chat completion: ", exception)
239
+ return await controller.returnLeft({ code: "UNKNOWN" })
240
+ }
241
+ }
242
+
243
+ async customChatCompletionStreaming(
244
+ options: CustomChatCompletionOptions,
245
+ ): Promise<CustomChatCompletionStreamingResult> {
246
+ const controller = controllerFromEitherType<CustomChatCompletionStreamingResult>()
247
+
248
+ const {
249
+ timeout = this.options.timeout,
250
+ abortSignal,
251
+ } = options
252
+ const openaiClient = this.openai
253
+ const logger = this.logger
254
+
255
+ const completionTube = new Tube<Completion>({
256
+ historyCount: Infinity,
257
+ replayHistory: true
258
+ })
259
+
260
+ const start = async (): Promise<void> => {
261
+ const { abortSignal: _, ...restOptions } = options
262
+ try {
263
+ // Pass the AbortSignal to the OpenAI request to support aborting
264
+ logger.log("Starting chat completion streaming.")
265
+ const response = await openaiClient.chat.completions.create(
266
+ {
267
+ ...restOptions,
268
+ stream: true,
269
+ },
270
+ {
271
+ timeout,
272
+ maxRetries: 0,
273
+ signal: abortSignal, // Include the abort signal in the request options
274
+ },
275
+ )
276
+ logger.log("Chat completion streaming started.")
277
+
278
+ // NOTE: 以下将 response 转换成 ReadableStream
279
+ const readableStream = streamTransformInMacroTask<Uint8Array, OriginalChatCompletionChunkResult>({
280
+ reader: response.toReadableStream().getReader(),
281
+ onChunk: (chunk, controller) => {
282
+ if (abortSignal !== undefined && abortSignal.aborted) {
283
+ logger.log("Original chat completion api's stream aborted by invoker.")
284
+ const error = new Error("Original chat completion api's stream aborted by invoker.")
285
+ controller.error(error)
286
+ return
287
+ }
288
+ if (chunk.done) {
289
+ logger.log("Original chat completion api's stream ended.")
290
+ controller.close()
291
+ return
292
+ }
293
+ const textDecoder = new TextDecoder()
294
+ const decoded = textDecoder.decode(chunk.value)
295
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
296
+ const parsed = JSON.parse(decoded) as OriginalChatCompletionChunkResult
297
+ // logger.log("Original chat completion api's stream chunk:", JSON.stringify(parsed))
298
+ controller.enqueue(parsed)
299
+ return
300
+ },
301
+ onError: (error) => {
302
+ logger.error("Error reading original chat completion api's stream:", error)
303
+ },
304
+ })
305
+
306
+ const latestCompletion: Completion = {
307
+ content: {
308
+ deltaList: [],
309
+ total: "",
310
+ },
311
+ token: {
312
+ deltaList: [],
313
+ total: 0,
314
+ },
315
+ }
316
+
317
+ streamConsumeInMacroTask({
318
+ readableStream,
319
+ onValue: async (chunk) => {
320
+ // If the signal is aborted, break and stop the stream
321
+ if (abortSignal !== undefined && abortSignal.aborted) {
322
+ throw new Error("Stream aborted by abort signal")
323
+ }
324
+ const choice = chunk.choices[0]
325
+ if (choice === undefined) {
326
+ // NOTE: sometimes there will be no choice in the chunk, just skip the chunk
327
+ // Example chunk:
328
+ // {
329
+ // "id": "chatcmpl-hTMAcJUVRpO5Etj64tdowhgpQalcP",
330
+ // "object": "chat.completion.chunk",
331
+ // "created": 1734481734,
332
+ // "model": "gpt-4o-mini",
333
+ // "choices": [],
334
+ // "usage": {
335
+ // "prompt_tokens": 2345,
336
+ // "completion_tokens": 90,
337
+ // "total_tokens": 2435,
338
+ // "prompt_tokens_details": {
339
+ // "cached_tokens": 0,
340
+ // "audio_tokens": 0
341
+ // },
342
+ // "completion_tokens_details": {
343
+ // "audio_tokens": 0
344
+ // }
345
+ // }
346
+ // }
347
+ return
348
+ }
349
+
350
+ const finishReason = choice.finish_reason
351
+ if (finishReason === "stop") {
352
+ // the model hit a natural stop point or a provided stop sequence, do nothing
353
+ }
354
+ if (finishReason === "length") {
355
+ // the maximum number of tokens specified in the request was reached
356
+ logger.error("Chat completion is too long.")
357
+ }
358
+ if (finishReason === "content_filter") {
359
+ logger.error("Chat completion is filtered.")
360
+ throw new Error("Chat completion is filtered.")
361
+ }
362
+
363
+ const refusal = choice.delta.refusal
364
+ if (refusal !== null && refusal !== undefined) {
365
+ logger.error(`Chat completion is refused: ${refusal}`)
366
+ throw new Error(`Chat completion is refused: ${refusal}`)
367
+ }
368
+
369
+ const deltaContent = choice.delta.content
370
+ if (deltaContent !== null && deltaContent !== undefined) {
371
+ latestCompletion.content = Aio.applyDeltaToTextContent(latestCompletion.content, {
372
+ type: "append",
373
+ text: deltaContent,
374
+ })
375
+ latestCompletion.token = Aio.applyDeltaToNumberContent(latestCompletion.token, {
376
+ type: "append",
377
+ value: 0,
378
+ })
379
+ await completionTube.pushData(structuredClone(latestCompletion))
380
+ }
381
+ },
382
+ onDone: async () => {
383
+ await completionTube.end()
384
+ },
385
+ onError: async (error) => {
386
+ await completionTube.pushError(error)
387
+ },
388
+ })
389
+ }
390
+ catch (exception) {
391
+ if (abortSignal !== undefined && abortSignal.aborted) {
392
+ logger.log("Original chat completion api's stream aborted by invoker.")
393
+ const error = new Error("Original chat completion api's stream aborted by invoker.")
394
+ await completionTube.pushError(error)
395
+ }
396
+ else {
397
+ logger.error("Error starting chat completion streaming: ", exception)
398
+ const error = new Error(`Error starting chat completion streaming, ${String(exception)}`)
399
+ await completionTube.pushError(error)
400
+ }
401
+ }
402
+ }
403
+ scheduleMacroTask({
404
+ task: async () => {
405
+ await start()
406
+ },
407
+ })
408
+
409
+ return await controller.returnRight({
410
+ completionTube,
411
+ })
412
+ }
413
+
414
+ /**
415
+ * 原始 embedding 方法的包装,将其最大重试次数固定为 0。
416
+ */
417
+ async originalEmbedding(
418
+ options: OriginalEmbeddingOptions
419
+ ): Promise<OriginalEmbeddingResult> {
420
+ const { abortSignal, ...restOptions } = options
421
+ try {
422
+ const response = await this.openai.embeddings.create({
423
+ ...restOptions,
424
+ }, {
425
+ maxRetries: 0,
426
+ signal: abortSignal,
427
+ })
428
+ return response
429
+ }
430
+ catch (exception) {
431
+ this.logger.error("Error creating embedding: ", exception)
432
+ throw exception
433
+ }
434
+ }
435
+
436
+ /**
437
+ * {@link https://platform.openai.com/docs/api-reference/embeddings | Embeddings - OpenAI API}
438
+ */
439
+ async customEmbedding(
440
+ options: CustomEmbeddingOptions
441
+ ): Promise<CustomEmbeddingResult> {
442
+ const controller = controllerFromEitherType<CustomEmbeddingResult>()
443
+ const logger = this.logger
444
+ try {
445
+ const {
446
+ abortSignal,
447
+ input,
448
+ model = "text-embedding-3-large",
449
+ // IMPORTANT: the number of dimensions should be 1536 currently, ask @kongxiangyan for more information.
450
+ dimensions = 1_536,
451
+ } = options
452
+ logger.log(`Creating embedding with model: ${model}, dimensions: ${dimensions}, input: ${input}`)
453
+ const response = await this.openai.embeddings.create({
454
+ input,
455
+ model,
456
+ dimensions,
457
+ }, {
458
+ maxRetries: 0,
459
+ signal: abortSignal,
460
+ })
461
+ logger.log("Get embedding response.")
462
+ // logger.log("Embedding response: ", JSON.stringify(response, null, 2))
463
+ if (response.data.length === 0) {
464
+ logger.error("Embedding is empty.")
465
+ return await controller.returnLeft({ code: "NO_EMBEDDING" })
466
+ }
467
+ if (response.model !== model) {
468
+ logger.error(`Model mismatch: ${response.model} !== ${model}`)
469
+ return await controller.returnLeft({ code: "MODEL_MISMATCH" })
470
+ }
471
+
472
+ const embedding = response.data[0]!.embedding
473
+ const promptTokens = response.usage.prompt_tokens
474
+ const totalTokens = response.usage.total_tokens
475
+ return await controller.returnRight({
476
+ embedding,
477
+ usage: {
478
+ promptTokens,
479
+ totalTokens,
480
+ },
481
+ })
482
+ }
483
+ catch (exception) {
484
+ logger.error("Error creating embedding: ", exception)
485
+ return await controller.returnLeft({ code: "UNKNOWN" })
486
+ }
487
+ }
488
+
489
+ /**
490
+ * {@link https://platform.openai.com/docs/api-reference/audio/createSpeech | Audio - OpenAI API}
491
+ */
492
+ async customSpeech(options: CustomSpeechOptions, stream?: boolean): Promise<Buffer> {
493
+ try {
494
+ const response = await this.openai.audio.speech.create({
495
+ ...options,
496
+ model: "tts-1",
497
+ voice: "alloy",
498
+ response_format: "opus",
499
+ }, {
500
+ stream: stream ?? false,
501
+ })
502
+ const buffer = Buffer.from(await response.arrayBuffer())
503
+ return buffer
504
+ }
505
+ catch (exception) {
506
+ this.logger.error("Error creating speech: ", exception)
507
+ throw exception
508
+ }
509
+ }
510
+ }