@citeark/agent 0.3.20

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 (347) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +128 -0
  3. package/data/dataset-source-registry.v1.json +300 -0
  4. package/dist/arkgraph/boot.js +6 -0
  5. package/dist/arkgraph/index.html +1 -0
  6. package/dist/arkgraph/viewer.css +1 -0
  7. package/dist/arkgraph/viewer.en.css +1 -0
  8. package/dist/arkgraph/viewer.en.js +49 -0
  9. package/dist/arkgraph/viewer.en.js.LEGAL.txt +56 -0
  10. package/dist/arkgraph/viewer.js +49 -0
  11. package/dist/arkgraph/viewer.js.LEGAL.txt +56 -0
  12. package/docker/claude-code/Dockerfile +97 -0
  13. package/docker/claude-code/codex-pro-relay.mjs +466 -0
  14. package/docker/claude-code/runtime-contract-check.mjs +79 -0
  15. package/docs/arkgraph-reading.md +79 -0
  16. package/docs/configuration.md +100 -0
  17. package/docs/integration.md +92 -0
  18. package/docs/maturity-plan.md +27 -0
  19. package/docs/npm-release.md +44 -0
  20. package/docs/paper-reading.md +40 -0
  21. package/docs/research-plan-granularity.md +27 -0
  22. package/docs/terminal.md +49 -0
  23. package/examples/toy-evaluation/compile-task.json +27 -0
  24. package/examples/toy-evaluation/paper.md +5 -0
  25. package/examples/toy-evaluation/repository/README.md +9 -0
  26. package/examples/toy-evaluation/repository/checkpoint.json +4 -0
  27. package/examples/toy-evaluation/repository/evaluate.py +17 -0
  28. package/examples/toy-evaluation/task.json +81 -0
  29. package/package.json +59 -0
  30. package/prompts/compile-research.md +58 -0
  31. package/prompts/execute-contract.md +72 -0
  32. package/prompts/execute-workspace-simple.md +51 -0
  33. package/prompts/execute-workspace.md +34 -0
  34. package/prompts/prepare-reproduction.md +82 -0
  35. package/prompts/repair-research.md +45 -0
  36. package/protocol/CAP.md +129 -0
  37. package/protocol/LICENSE +12 -0
  38. package/protocol/MAPPINGS.md +72 -0
  39. package/protocol/README.md +38 -0
  40. package/protocol/conformance-v2.0-alpha.1.json +36 -0
  41. package/protocol/examples/arkgraph/checkpoint-evaluation.json +309 -0
  42. package/protocol/examples/arkgraph/fixtures.mjs +49 -0
  43. package/protocol/examples/arkgraph/paper-free.json +291 -0
  44. package/protocol/examples/arkgraph/partial-failure.json +344 -0
  45. package/protocol/examples/arkgraph/training-evaluation.json +443 -0
  46. package/protocol/profiles/agent-trace.md +16 -0
  47. package/protocol/profiles/computational-run.md +16 -0
  48. package/protocol/profiles/core.md +15 -0
  49. package/protocol/profiles/public-bundle.md +18 -0
  50. package/protocol/profiles/reproduction.md +29 -0
  51. package/protocol/profiles/research-compilation.md +44 -0
  52. package/protocol/profiles/research-plan.md +39 -0
  53. package/protocol/profiles/restricted-evidence.md +15 -0
  54. package/runtime/bootstrap-autodl-runtime.sh +314 -0
  55. package/runtime/create-runtime-venv.sh +41 -0
  56. package/runtime/install-local-cpu-runtime.sh +23 -0
  57. package/runtime/install-scientific-runtime.sh +153 -0
  58. package/runtime/mineru/parse.py +62 -0
  59. package/runtime/mineru/requirements.txt +4 -0
  60. package/runtime/requirements-baseline.txt +38 -0
  61. package/schemas/cap/v2/activity.schema.json +47 -0
  62. package/schemas/cap/v2/agent.schema.json +32 -0
  63. package/schemas/cap/v2/assertion.schema.json +110 -0
  64. package/schemas/cap/v2/descriptor.schema.json +243 -0
  65. package/schemas/cap/v2/entity.schema.json +64 -0
  66. package/schemas/cap/v2/manifest.schema.json +67 -0
  67. package/schemas/cap/v2/relation.schema.json +82 -0
  68. package/schemas/compute-catalog.schema.json +63 -0
  69. package/schemas/compute-decision.schema.json +27 -0
  70. package/schemas/execution-contract.schema.json +1024 -0
  71. package/schemas/research-card.schema.json +30 -0
  72. package/schemas/research-inventory-draft.schema.json +366 -0
  73. package/schemas/research.schema.json +1044 -0
  74. package/schemas/result.schema.json +173 -0
  75. package/schemas/verification-policy.schema.json +47 -0
  76. package/schemas/verified-conclusion.schema.json +58 -0
  77. package/schemas/workspace-summary.schema.json +24 -0
  78. package/scripts/build-arkgraph-view.mjs +12 -0
  79. package/scripts/check-execution-feasibility.mjs +24 -0
  80. package/scripts/check-syntax.mjs +15 -0
  81. package/scripts/deterministic-asset-preparation.py +438 -0
  82. package/scripts/package-cap.mjs +23 -0
  83. package/scripts/package-local-agent.mjs +23 -0
  84. package/scripts/preview-arkgraph.mjs +25 -0
  85. package/scripts/replay-research-compiler-candidate.mjs +134 -0
  86. package/scripts/review-compiler-sources.mjs +44 -0
  87. package/scripts/run-asset-preparation.sh +17 -0
  88. package/scripts/run-research-plan.mjs +98 -0
  89. package/scripts/validate-asset-preparation.py +290 -0
  90. package/scripts/verify-local-runtime.mjs +57 -0
  91. package/scripts/verify-npm-package.mjs +57 -0
  92. package/src/adapters/paper2agent.mjs +107 -0
  93. package/src/assets/cache.mjs +159 -0
  94. package/src/assets/compute.mjs +98 -0
  95. package/src/assets/executor.mjs +145 -0
  96. package/src/assets/lifecycle.mjs +213 -0
  97. package/src/assets/manifest.mjs +242 -0
  98. package/src/assets/opportunistic-preparation.mjs +81 -0
  99. package/src/assets/plan.mjs +411 -0
  100. package/src/assets/prompts.mjs +29 -0
  101. package/src/assets/public-asset-probe.mjs +525 -0
  102. package/src/assets/qualification.mjs +119 -0
  103. package/src/assets/readiness.mjs +130 -0
  104. package/src/assets/reproduction-admission.mjs +355 -0
  105. package/src/assets/requirements.mjs +152 -0
  106. package/src/assets/source-grounding.mjs +341 -0
  107. package/src/assets/source-policy.mjs +118 -0
  108. package/src/autodl/client.mjs +260 -0
  109. package/src/autodl/ssh.mjs +380 -0
  110. package/src/autodl/tools.mjs +129 -0
  111. package/src/cap/redaction.mjs +38 -0
  112. package/src/cap/v2/archive.mjs +152 -0
  113. package/src/cap/v2/attestation.mjs +204 -0
  114. package/src/cap/v2/canonical-json.mjs +114 -0
  115. package/src/cap/v2/compilation-artifact.mjs +240 -0
  116. package/src/cap/v2/core.mjs +282 -0
  117. package/src/cap/v2/measurement-assessment-records.mjs +23 -0
  118. package/src/cap/v2/pipeline-artifact.mjs +922 -0
  119. package/src/cap/v2/read.mjs +41 -0
  120. package/src/cap/v2/reassessment-artifact.mjs +383 -0
  121. package/src/cap/v2/research-artifact.mjs +231 -0
  122. package/src/cap/v2/research-map-records.mjs +46 -0
  123. package/src/cap/v2/research-object-records.mjs +163 -0
  124. package/src/cap/v2/research-records.mjs +187 -0
  125. package/src/cap/v2/verify.mjs +642 -0
  126. package/src/cli.mjs +1146 -0
  127. package/src/compute/autodl-pro-compiler.mjs +347 -0
  128. package/src/compute/autodl-pro-executor.mjs +459 -0
  129. package/src/compute/autodl-pro-job.mjs +843 -0
  130. package/src/compute/autodl-pro-network.mjs +295 -0
  131. package/src/compute/autodl-pro-remote.mjs +810 -0
  132. package/src/compute/autodl-pro-staging.mjs +117 -0
  133. package/src/compute/campaign.mjs +110 -0
  134. package/src/compute/catalog.mjs +123 -0
  135. package/src/compute/checkpoint-protocol.mjs +154 -0
  136. package/src/compute/codex-account-lock.mjs +111 -0
  137. package/src/compute/codex-account-session.mjs +107 -0
  138. package/src/compute/compiler-profile.mjs +38 -0
  139. package/src/compute/compiler-router.mjs +23 -0
  140. package/src/compute/coordinator-recovery.mjs +210 -0
  141. package/src/compute/executor-router.mjs +29 -0
  142. package/src/compute/gcp-batch-compiler.mjs +685 -0
  143. package/src/compute/gcp-batch-executor.mjs +1215 -0
  144. package/src/compute/gcp-batch-failure.mjs +92 -0
  145. package/src/compute/gcp-batch-job.mjs +527 -0
  146. package/src/compute/gcp-batch-lifecycle.mjs +81 -0
  147. package/src/compute/gcp-checkpoint-worker.mjs +1633 -0
  148. package/src/compute/local-codex-compiler.mjs +52 -0
  149. package/src/compute/measurement-hardware.mjs +128 -0
  150. package/src/compute/remote-attempt.mjs +226 -0
  151. package/src/compute/requirements.mjs +124 -0
  152. package/src/compute/research-phases.mjs +48 -0
  153. package/src/compute/scheduler.mjs +452 -0
  154. package/src/compute/shared-workloads.mjs +26 -0
  155. package/src/compute/stage-archive.mjs +79 -0
  156. package/src/contracts/campaign-contract.mjs +52 -0
  157. package/src/contracts/execution-contract.mjs +819 -0
  158. package/src/contracts/execution-mode.mjs +19 -0
  159. package/src/contracts/execution-timeouts.mjs +45 -0
  160. package/src/contracts/execution-workload.mjs +68 -0
  161. package/src/contracts/preflight-schema.mjs +25 -0
  162. package/src/contracts/public-contract.mjs +63 -0
  163. package/src/contracts/subject-tags.mjs +31 -0
  164. package/src/dashboard/data.mjs +898 -0
  165. package/src/dashboard/server.mjs +79 -0
  166. package/src/dashboard/static/dashboard.css +366 -0
  167. package/src/dashboard/static/dashboard.js +560 -0
  168. package/src/dashboard/static/index.html +85 -0
  169. package/src/deployment/community-policy.mjs +9 -0
  170. package/src/deployment/environment.mjs +112 -0
  171. package/src/deployment/guided.mjs +98 -0
  172. package/src/deployment/handoff.mjs +102 -0
  173. package/src/deployment/local-contract.mjs +31 -0
  174. package/src/deployment/local.mjs +100 -0
  175. package/src/deployment/prepare.mjs +46 -0
  176. package/src/deployment/recipe.mjs +108 -0
  177. package/src/deployment/supplement.mjs +51 -0
  178. package/src/deployment/terminal.mjs +43 -0
  179. package/src/diagnosis/renderer.mjs +75 -0
  180. package/src/diagnosis/target-failure.mjs +46 -0
  181. package/src/evidence/parser-registry.mjs +54 -0
  182. package/src/evidence/parsers/fasttext-classification.mjs +82 -0
  183. package/src/evidence/parsers/json-scalar.mjs +96 -0
  184. package/src/evidence/parsers/simcse-senteval.mjs +104 -0
  185. package/src/evidence/parsers/starspace-classification.mjs +78 -0
  186. package/src/evidence/registry.mjs +147 -0
  187. package/src/execution/runner-audit.mjs +473 -0
  188. package/src/gcp/auth.mjs +106 -0
  189. package/src/gcp/batch-client.mjs +120 -0
  190. package/src/gcp/resource-discovery.mjs +177 -0
  191. package/src/gcp/rest.mjs +82 -0
  192. package/src/gcp/secret-manager.mjs +34 -0
  193. package/src/gcp/signed-url.mjs +133 -0
  194. package/src/gcp/storage.mjs +220 -0
  195. package/src/graph/command.mjs +41 -0
  196. package/src/graph/execution.mjs +97 -0
  197. package/src/graph/model.mjs +37 -0
  198. package/src/graph/presentation.mjs +110 -0
  199. package/src/graph/query.mjs +159 -0
  200. package/src/graph/research-relations.mjs +69 -0
  201. package/src/graph/source-page.mjs +12 -0
  202. package/src/graph/source-preview.mjs +34 -0
  203. package/src/graph/validate.mjs +76 -0
  204. package/src/job.mjs +496 -0
  205. package/src/network/autodl-routing-proxy.mjs +462 -0
  206. package/src/network/egress-proxy.mjs +158 -0
  207. package/src/observability/event-contract.mjs +230 -0
  208. package/src/observability/pipeline-monitor.mjs +166 -0
  209. package/src/pipeline/orchestrator.mjs +1281 -0
  210. package/src/pipeline/recovery-error.mjs +11 -0
  211. package/src/pipeline/replay.mjs +304 -0
  212. package/src/pipeline/shared-execution.mjs +115 -0
  213. package/src/pipeline/stage-checkpoint.mjs +86 -0
  214. package/src/pipeline/stage-recovery.mjs +101 -0
  215. package/src/pipeline/targets.mjs +110 -0
  216. package/src/process.mjs +143 -0
  217. package/src/protocol.mjs +312 -0
  218. package/src/provider/codex-account.mjs +44 -0
  219. package/src/provider/codex-completion.mjs +49 -0
  220. package/src/provider/completion.mjs +292 -0
  221. package/src/provider/model-client.mjs +44 -0
  222. package/src/provider/model-route.mjs +29 -0
  223. package/src/provider/openrouter-readiness.mjs +189 -0
  224. package/src/provider/reader-bridge.mjs +35 -0
  225. package/src/provider/relay.mjs +263 -0
  226. package/src/provider/runtime-auth.mjs +40 -0
  227. package/src/public/cap.d.mts +90 -0
  228. package/src/public/cap.mjs +12 -0
  229. package/src/public/contracts.d.mts +2 -0
  230. package/src/public/host.mjs +171 -0
  231. package/src/public/operations.d.mts +11 -0
  232. package/src/public/presentation.d.mts +4 -0
  233. package/src/records/views.mjs +26 -0
  234. package/src/remote/command.mjs +178 -0
  235. package/src/remote/ssh.mjs +59 -0
  236. package/src/repository-origin.mjs +81 -0
  237. package/src/reproduction/evidence-feedback.mjs +96 -0
  238. package/src/reproduction/incomplete-initialization.mjs +25 -0
  239. package/src/reproduction/lifecycle.mjs +253 -0
  240. package/src/reproduction/plan.mjs +132 -0
  241. package/src/reproduction/prompts.mjs +70 -0
  242. package/src/reproduction/runner.mjs +188 -0
  243. package/src/reproduction/summary.mjs +130 -0
  244. package/src/reproduction/workspace-mode.mjs +7 -0
  245. package/src/research/automatic-admission.mjs +156 -0
  246. package/src/research/compiler-coverage.mjs +85 -0
  247. package/src/research/compiler-failure.mjs +24 -0
  248. package/src/research/compiler-normalization-guards.mjs +112 -0
  249. package/src/research/compiler-repair.mjs +3 -0
  250. package/src/research/compiler.mjs +853 -0
  251. package/src/research/continuation-selection.mjs +26 -0
  252. package/src/research/execution-graph-context.mjs +43 -0
  253. package/src/research/experiment-importance.mjs +15 -0
  254. package/src/research/inventory-handoff.mjs +104 -0
  255. package/src/research/inventory-revisions.mjs +32 -0
  256. package/src/research/mineru-local.mjs +73 -0
  257. package/src/research/paper-command.mjs +19 -0
  258. package/src/research/paper-markdown.mjs +180 -0
  259. package/src/research/paper-source-map.mjs +69 -0
  260. package/src/research/planning-policy.mjs +88 -0
  261. package/src/research/reference-materials.mjs +11 -0
  262. package/src/research/reproduction-scope.mjs +30 -0
  263. package/src/research/research-map.mjs +94 -0
  264. package/src/research/research-objects.mjs +88 -0
  265. package/src/research/source-discovery.mjs +646 -0
  266. package/src/research/source-observations.mjs +75 -0
  267. package/src/research/source-review-cli-mcp.mjs +26 -0
  268. package/src/research/source-review-input.mjs +209 -0
  269. package/src/research/source-review-local-codex.mjs +36 -0
  270. package/src/research/source-review-model.mjs +70 -0
  271. package/src/research/source-review.mjs +173 -0
  272. package/src/research/structure.mjs +3163 -0
  273. package/src/research-card/renderer.mjs +277 -0
  274. package/src/research-card/verified-conclusion.mjs +143 -0
  275. package/src/results/output-registry.mjs +183 -0
  276. package/src/runtime/claude-code.mjs +52 -0
  277. package/src/runtime/codex-capacity-retry.mjs +87 -0
  278. package/src/runtime/codex.mjs +64 -0
  279. package/src/runtime/config.mjs +157 -0
  280. package/src/runtime/final-output.mjs +40 -0
  281. package/src/runtime/index.mjs +21 -0
  282. package/src/runtime/local-codex.mjs +74 -0
  283. package/src/runtime/opencode.mjs +95 -0
  284. package/src/runtime/prompt.mjs +13 -0
  285. package/src/sandbox/docker.mjs +363 -0
  286. package/src/settings/command.mjs +297 -0
  287. package/src/settings/store.mjs +119 -0
  288. package/src/telemetry/pricing.mjs +68 -0
  289. package/src/telemetry/usage.mjs +265 -0
  290. package/src/terminal/events.mjs +97 -0
  291. package/src/terminal/input.mjs +40 -0
  292. package/src/terminal/plain.mjs +40 -0
  293. package/src/terminal/remote-stream.mjs +22 -0
  294. package/src/terminal/screen.mjs +214 -0
  295. package/src/terminal/transcript.mjs +69 -0
  296. package/src/util.mjs +107 -0
  297. package/src/verification/ai-assessor.mjs +534 -0
  298. package/src/verification/claim-evaluator.mjs +242 -0
  299. package/src/verification/evidence-context.mjs +165 -0
  300. package/src/verification/evidence-reader.mjs +95 -0
  301. package/src/verification/integrity.mjs +570 -0
  302. package/src/verification/tolerance.mjs +32 -0
  303. package/src/workloads/cpu-research-preparation.mjs +56 -0
  304. package/src/workloads/definition.mjs +74 -0
  305. package/src/workloads/phase-aware-reproduction.mjs +46 -0
  306. package/src/workloads/reproduction.mjs +85 -0
  307. package/src/workspace/command.mjs +242 -0
  308. package/src/workspace/control.mjs +49 -0
  309. package/src/workspace/entry.mjs +28 -0
  310. package/src/workspace/input.mjs +93 -0
  311. package/src/workspace/interactive.mjs +94 -0
  312. package/src/workspace/jobs.mjs +418 -0
  313. package/src/workspace/session.mjs +97 -0
  314. package/src/workspace/worker.mjs +137 -0
  315. package/ui/arkgraph/ambient-motion.mjs +10 -0
  316. package/ui/arkgraph/app.jsx +153 -0
  317. package/ui/arkgraph/boot.js +6 -0
  318. package/ui/arkgraph/camera-motion.mjs +20 -0
  319. package/ui/arkgraph/context-reveal.mjs +39 -0
  320. package/ui/arkgraph/details.css +3 -0
  321. package/ui/arkgraph/entry.jsx +28 -0
  322. package/ui/arkgraph/experiment-curves.mjs +17 -0
  323. package/ui/arkgraph/experiment-selection.mjs +15 -0
  324. package/ui/arkgraph/experiment-style.css +26 -0
  325. package/ui/arkgraph/experiment-ui.jsx +32 -0
  326. package/ui/arkgraph/frame.html +1 -0
  327. package/ui/arkgraph/graph-gestures.mjs +62 -0
  328. package/ui/arkgraph/label-layout.mjs +57 -0
  329. package/ui/arkgraph/locales/en.json +229 -0
  330. package/ui/arkgraph/locales/source-types.json +15 -0
  331. package/ui/arkgraph/localization-build.mjs +27 -0
  332. package/ui/arkgraph/material-build.mjs +23 -0
  333. package/ui/arkgraph/material-colors.mjs +39 -0
  334. package/ui/arkgraph/material-style.css +15 -0
  335. package/ui/arkgraph/open-graph.jsx +326 -0
  336. package/ui/arkgraph/outline.jsx +49 -0
  337. package/ui/arkgraph/package-lock.json +888 -0
  338. package/ui/arkgraph/package.json +17 -0
  339. package/ui/arkgraph/reading-layout.mjs +130 -0
  340. package/ui/arkgraph/reading-presentation.mjs +73 -0
  341. package/ui/arkgraph/record-detail.css +51 -0
  342. package/ui/arkgraph/record-details.jsx +29 -0
  343. package/ui/arkgraph/research-types.mjs +31 -0
  344. package/ui/arkgraph/selection-mark.jsx +6 -0
  345. package/ui/arkgraph/soft-spine.mjs +26 -0
  346. package/ui/arkgraph/steering-style.css +187 -0
  347. package/ui/arkgraph/style.css +272 -0
@@ -0,0 +1,646 @@
1
+ import { captureProcess } from "../process.mjs";
2
+ import { externalRequestObservation } from "../observability/event-contract.mjs";
3
+
4
+ const GITHUB_API = "https://api.github.com";
5
+ const MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
6
+ const RESERVED_GITHUB_OWNERS = new Set([
7
+ "collections",
8
+ "features",
9
+ "login",
10
+ "marketplace",
11
+ "orgs",
12
+ "search",
13
+ "settings",
14
+ "topics",
15
+ ]);
16
+
17
+ export class SourceDiscoveryError extends Error {
18
+ constructor(message, details = {}) {
19
+ super(message);
20
+ this.name = "SourceDiscoveryError";
21
+ this.details = details;
22
+ this.reasonCode = details.reasonCode;
23
+ this.retryable = details.retryable === true;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * 在 Research Compiler 克隆仓库之前验证论文原文声明的 GitHub 仓库。
29
+ * 多链接论文先使用链接附近的“代码发布/复现材料”角色语义排除引用实现、
30
+ * 基线和依赖;只有角色唯一且分差充足时才采用。论文没有直链时,GitHub
31
+ * 搜索只用于发现候选,必须再由固定论文中的完整标题、arXiv ID 和作者身份
32
+ * 等相互独立证据完成归属验证;不会仅凭标题搜索结果或 README 自述采用仓库。
33
+ */
34
+ export async function discoverRepository({
35
+ paperPath,
36
+ paperUri,
37
+ title,
38
+ fetchImpl = fetch,
39
+ extractPaperText = extractPdfText,
40
+ sleep = defaultSleep,
41
+ now = () => new Date(),
42
+ observe = () => {},
43
+ }) {
44
+ const diagnostics = [];
45
+ const externalRequests = [];
46
+ const recordExternalRequest = (observation) => {
47
+ if (externalRequests.length < 50) externalRequests.push(observation);
48
+ try {
49
+ observe(observation);
50
+ } catch {
51
+ // 可观测性回调不能改变来源发现本身的科学或安全决策。
52
+ }
53
+ };
54
+ let paperText = "";
55
+ if (paperPath) {
56
+ try {
57
+ paperText = await extractPaperText(paperPath);
58
+ } catch (error) {
59
+ diagnostics.push(`paper-text: ${errorMessage(error)}`);
60
+ throw new SourceDiscoveryError("无法解析固定论文文本,暂不能判断作者仓库", {
61
+ reasonCode: "paper_text_extraction_failed",
62
+ retryable: true,
63
+ diagnostics,
64
+ externalRequests,
65
+ });
66
+ }
67
+ }
68
+
69
+ let paperCandidates = extractGitHubRepositoryCandidates(paperText);
70
+ let resolutionMethod = "paper-link";
71
+ if (paperCandidates.length === 0) {
72
+ let evidenceResolved;
73
+ try {
74
+ evidenceResolved = await discoverAuthorOwnedRepository({
75
+ paperText,
76
+ paperUri,
77
+ title,
78
+ fetchImpl,
79
+ sleep,
80
+ now,
81
+ diagnostics,
82
+ observe: recordExternalRequest,
83
+ });
84
+ } catch (error) {
85
+ if (error?.retryable !== true) throw error;
86
+ throw new SourceDiscoveryError("作者仓库证据查询暂时不可用", {
87
+ reasonCode: "repository_identity_resolution_unavailable",
88
+ retryable: true,
89
+ diagnostics,
90
+ externalRequests,
91
+ });
92
+ }
93
+ if (evidenceResolved) {
94
+ paperCandidates = [evidenceResolved];
95
+ resolutionMethod = "author-owned-paper-matched";
96
+ }
97
+ }
98
+ const directUrls = paperCandidates.map((candidate) => candidate.url);
99
+ const resolvedCandidate = resolvePaperDeclaredRepositoryCandidate(paperCandidates);
100
+ if (!resolvedCandidate) {
101
+ throw new SourceDiscoveryError(
102
+ directUrls.length
103
+ ? "论文原文包含多个 GitHub 仓库链接,无法在不猜测的前提下确定官方实现"
104
+ : "论文原文未明确声明 GitHub 代码仓库",
105
+ {
106
+ reasonCode: directUrls.length
107
+ ? "ambiguous_paper_repository_links"
108
+ : "repository_identity_not_resolved",
109
+ directUrlCount: directUrls.length,
110
+ directUrls: directUrls.slice(0, 20),
111
+ diagnostics,
112
+ externalRequests,
113
+ },
114
+ );
115
+ }
116
+
117
+ const identity = githubIdentity(resolvedCandidate.url);
118
+ let selected;
119
+ let accessVerification = "github-api";
120
+ try {
121
+ selected = await requestJson(
122
+ fetchImpl,
123
+ `${GITHUB_API}/repos/${identity.owner}/${identity.repo}`,
124
+ {
125
+ sleep,
126
+ now,
127
+ observe: recordExternalRequest,
128
+ service: "github",
129
+ operation: "repository_metadata",
130
+ },
131
+ );
132
+ } catch (error) {
133
+ diagnostics.push(`${identity.owner}/${identity.repo}: ${errorMessage(error)}`);
134
+ try {
135
+ await request(
136
+ fetchImpl,
137
+ `${resolvedCandidate.url}.git/info/refs?service=git-upload-pack`,
138
+ {
139
+ sleep,
140
+ now,
141
+ observe: recordExternalRequest,
142
+ service: "github",
143
+ operation: "git_smart_http_probe",
144
+ headers: { accept: "application/x-git-upload-pack-advertisement" },
145
+ },
146
+ );
147
+ selected = {
148
+ html_url: resolvedCandidate.url,
149
+ full_name: `${identity.owner}/${identity.repo}`,
150
+ private: false,
151
+ disabled: false,
152
+ };
153
+ accessVerification = "git-smart-http";
154
+ diagnostics.push(`${identity.owner}/${identity.repo}: Git smart HTTP access verified after API metadata failure`);
155
+ } catch (gitError) {
156
+ diagnostics.push(`${identity.owner}/${identity.repo} git-smart-http: ${errorMessage(gitError)}`);
157
+ throw new SourceDiscoveryError(
158
+ "论文原文声明的 GitHub 仓库暂时无法完成公开可访问性验证",
159
+ {
160
+ reasonCode: "paper_declared_repository_verification_unavailable",
161
+ retryable: error?.retryable === true || gitError?.retryable === true,
162
+ directUrlCount: directUrls.length,
163
+ directUrls,
164
+ diagnostics,
165
+ externalRequests,
166
+ },
167
+ );
168
+ }
169
+ }
170
+ if (selected.private || selected.disabled) {
171
+ throw new SourceDiscoveryError(
172
+ "论文原文声明的 GitHub 仓库不是可执行的公开仓库",
173
+ {
174
+ reasonCode: "paper_declared_repository_not_public",
175
+ directUrlCount: directUrls.length,
176
+ directUrls,
177
+ diagnostics,
178
+ externalRequests,
179
+ },
180
+ );
181
+ }
182
+ const htmlUrl = canonicalGitHubRepositoryUrl(selected.html_url) ?? resolvedCandidate.url;
183
+ return {
184
+ url: `${htmlUrl}.git`,
185
+ htmlUrl,
186
+ fullName: selected.full_name ?? `${identity.owner}/${identity.repo}`,
187
+ defaultBranch: selected.default_branch ?? null,
188
+ licenseSpdx:
189
+ selected.license?.spdx_id &&
190
+ selected.license.spdx_id !== "NOASSERTION"
191
+ ? selected.license.spdx_id
192
+ : null,
193
+ archived: Boolean(selected.archived),
194
+ method: `${resolutionMethod === "author-owned-paper-matched"
195
+ ? "author-owned-paper-matched"
196
+ : directUrls.length === 1
197
+ ? "paper-declared"
198
+ : "paper-declared-role-resolved"}+${accessVerification === "github-api"
199
+ ? "github-verification"
200
+ : "git-smart-http-verification"}`,
201
+ confidence: resolutionMethod === "author-owned-paper-matched"
202
+ ? "evidence-resolved"
203
+ : directUrls.length === 1
204
+ ? "paper-declared"
205
+ : "paper-declared-role-resolved",
206
+ evidence: {
207
+ directPaperLink: resolutionMethod === "paper-link",
208
+ source: "fixed-paper-text",
209
+ accessVerification,
210
+ directPaperLinkCount: resolutionMethod === "paper-link" ? directUrls.length : 0,
211
+ ...(resolutionMethod === "author-owned-paper-matched" ? {
212
+ identityResolution: {
213
+ score: resolvedCandidate.score,
214
+ signals: resolvedCandidate.signals,
215
+ },
216
+ } : {}),
217
+ ...(directUrls.length > 1 ? {
218
+ roleResolution: {
219
+ score: resolvedCandidate.score,
220
+ signals: resolvedCandidate.signals,
221
+ },
222
+ } : {}),
223
+ },
224
+ diagnostics,
225
+ externalRequests,
226
+ };
227
+ }
228
+
229
+ async function discoverAuthorOwnedRepository({
230
+ paperText,
231
+ paperUri,
232
+ title,
233
+ fetchImpl,
234
+ sleep,
235
+ now,
236
+ diagnostics,
237
+ observe,
238
+ }) {
239
+ const query = githubEvidenceSearchQuery(title, paperText);
240
+ if (!query) return null;
241
+ let search;
242
+ try {
243
+ search = await requestJson(
244
+ fetchImpl,
245
+ `${GITHUB_API}/search/repositories?q=${encodeURIComponent(query)}&per_page=5`,
246
+ { sleep, now, observe, service: "github", operation: "repository_search" },
247
+ );
248
+ } catch (error) {
249
+ diagnostics.push(`github-evidence-search: ${errorMessage(error)}`);
250
+ if (error?.retryable === true) throw error;
251
+ return null;
252
+ }
253
+ const fixedPaper = normalizeIdentityText(paperText.slice(0, 30_000));
254
+ const fixedTitleCandidates = paperTitleCandidates(title, paperText);
255
+ const fixedArxivId = arxivId(paperUri) ?? arxivId(paperText);
256
+ const evaluated = [];
257
+ let retryableCandidateFailure = false;
258
+ for (const candidate of Array.isArray(search?.items) ? search.items.slice(0, 5) : []) {
259
+ if (candidate?.private || candidate?.disabled || !candidate?.full_name) continue;
260
+ try {
261
+ const [readme, owner] = await Promise.all([
262
+ requestJson(fetchImpl, `${GITHUB_API}/repos/${candidate.full_name}/readme`, {
263
+ sleep, now, observe, service: "github", operation: "repository_readme",
264
+ }),
265
+ candidate.owner?.url
266
+ ? requestJson(fetchImpl, candidate.owner.url, {
267
+ sleep, now, observe, service: "github", operation: "repository_owner",
268
+ })
269
+ : Promise.resolve(null),
270
+ ]);
271
+ const readmeText = decodeGitHubContent(readme);
272
+ const heading = readmeText.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? candidate.name ?? "";
273
+ const normalizedHeading = normalizeIdentityText(heading);
274
+ const ownerName = typeof owner?.name === "string" ? owner.name.trim() : "";
275
+ const normalizedOwnerName = normalizeIdentityText(ownerName);
276
+ const readmeArxivId = arxivId(readmeText);
277
+ let score = 0;
278
+ const signals = [];
279
+ if (normalizedHeading.length >= 24 && fixedPaper.includes(normalizedHeading)) {
280
+ score += 10;
281
+ signals.push("exact-title-in-fixed-paper");
282
+ } else if (fixedTitleCandidates.some((candidateTitle) => titleIdentitySimilarity(heading, candidateTitle) >= 0.9)) {
283
+ score += 10;
284
+ signals.push("equivalent-title-in-fixed-paper");
285
+ }
286
+ if (fixedArxivId && readmeArxivId === fixedArxivId) {
287
+ score += 10;
288
+ signals.push("matching-arxiv-id");
289
+ }
290
+ if (normalizedOwnerName.length >= 8 && fixedPaper.includes(normalizedOwnerName)) {
291
+ score += 7;
292
+ signals.push("repository-owner-is-paper-author");
293
+ }
294
+ if (/\b(?:official implementation|code for|implementation of)\b/i.test(readmeText)) {
295
+ score += 2;
296
+ signals.push("readme-implementation-claim");
297
+ }
298
+ evaluated.push({
299
+ url: canonicalGitHubRepositoryUrl(candidate.html_url),
300
+ score,
301
+ signals,
302
+ });
303
+ } catch (error) {
304
+ diagnostics.push(`${candidate.full_name}: ${errorMessage(error)}`);
305
+ retryableCandidateFailure ||= error?.retryable === true;
306
+ }
307
+ }
308
+ const ranked = evaluated
309
+ .filter((candidate) => candidate.url)
310
+ .sort((left, right) => right.score - left.score);
311
+ const [first, second] = ranked;
312
+ const resolved = first && first.score >= 17 && first.score - (second?.score ?? 0) >= 5
313
+ ? first
314
+ : null;
315
+ if (!resolved && retryableCandidateFailure) {
316
+ const error = new Error("GitHub 候选仓库证据查询暂时不可用");
317
+ error.retryable = true;
318
+ throw error;
319
+ }
320
+ return resolved;
321
+ }
322
+
323
+ export function extractGitHubRepositoryUrls(value) {
324
+ return extractGitHubRepositoryCandidates(value).map((candidate) => candidate.url);
325
+ }
326
+
327
+ export function extractGitHubRepositoryCandidates(value) {
328
+ if (typeof value !== "string" || !value) return [];
329
+ const candidates = new Map();
330
+ // pdftotext commonly wraps a printed repository URL immediately after the
331
+ // host or slash. Accept whitespace at path boundaries, then remove it before
332
+ // URL parsing. This stays deliberately narrower than joining arbitrary PDF
333
+ // lines so unrelated two-column text cannot be mistaken for a repository.
334
+ const expression = /https?:\/\/(?:www\.)?github\.com\/\s*[a-zA-Z0-9_.-]+\s*\/\s*[a-zA-Z0-9_.-]+/g;
335
+ for (const match of value.matchAll(expression)) {
336
+ const canonical = canonicalGitHubRepositoryUrl(match[0].replaceAll(/\s+/g, ""));
337
+ if (!canonical) continue;
338
+ // Repository-role language normally precedes the link. Keep the look-ahead
339
+ // deliberately short so one later release statement cannot lend its role
340
+ // to an earlier citation on the same page.
341
+ const start = Math.max(0, (match.index ?? 0) - 260);
342
+ const end = Math.min(value.length, (match.index ?? 0) + match[0].length + 40);
343
+ const localContext = value.slice(start, end);
344
+ const footnoteContext = repositoryFootnoteContext(value, match.index ?? 0);
345
+ const role = repositoryRoleEvidence(
346
+ footnoteContext ? `${footnoteContext}\n${localContext}` : localContext,
347
+ );
348
+ const immediatePrefix = value
349
+ .slice(Math.max(0, (match.index ?? 0) - 140), match.index ?? 0)
350
+ .replaceAll(/\s+/g, " ")
351
+ .toLowerCase();
352
+ if (/[a-z0-9][a-z0-9_.+ -]{1,100}\s+is\s+(?:publicly\s+)?available\s+at\s*$/.test(immediatePrefix)) {
353
+ role.score += 8;
354
+ role.signals.push("named-artifact-availability");
355
+ }
356
+ const current = candidates.get(canonical);
357
+ if (!current || role.score > current.score) {
358
+ candidates.set(canonical, { url: canonical, ...role });
359
+ }
360
+ }
361
+ return [...candidates.values()];
362
+ }
363
+
364
+ function repositoryFootnoteContext(value, linkIndex) {
365
+ const lineStart = value.lastIndexOf("\n", linkIndex - 1) + 1;
366
+ const marker = value.slice(lineStart, linkIndex).trim().match(/^(\d{1,2})$/)?.[1];
367
+ if (!marker) return "";
368
+ const priorStart = Math.max(0, lineStart - 8_000);
369
+ const prior = value.slice(priorStart, lineStart);
370
+ const reference = new RegExp(`(?:[a-zA-Z)\\]])\\.?${marker}(?=\\s|$)`, "g");
371
+ let best = "";
372
+ let bestScore = Number.NEGATIVE_INFINITY;
373
+ for (const match of prior.matchAll(reference)) {
374
+ const context = prior.slice(
375
+ Math.max(0, (match.index ?? 0) - 650),
376
+ Math.min(prior.length, (match.index ?? 0) + match[0].length + 80),
377
+ );
378
+ const score = repositoryRoleEvidence(context).score;
379
+ if (score > bestScore) {
380
+ best = context;
381
+ bestScore = score;
382
+ }
383
+ }
384
+ return bestScore > 0 ? best : "";
385
+ }
386
+
387
+ export function canonicalGitHubRepositoryUrl(value) {
388
+ try {
389
+ const url = new URL(value);
390
+ if (url.protocol !== "https:" || !new Set(["github.com", "www.github.com"]).has(url.hostname.toLowerCase())) {
391
+ return null;
392
+ }
393
+ const [owner, rawRepo] = url.pathname.split("/").filter(Boolean);
394
+ const repo = rawRepo
395
+ ?.replace(/[.,;:!?]+$/g, "")
396
+ .replace(/\.git$/i, "");
397
+ if (!owner || !repo || RESERVED_GITHUB_OWNERS.has(owner.toLowerCase())) return null;
398
+ if (!/^[a-zA-Z0-9_.-]+$/.test(owner) || !/^[a-zA-Z0-9_.-]+$/.test(repo)) return null;
399
+ return `https://github.com/${owner}/${repo}`;
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+
405
+ function githubIdentity(value) {
406
+ const canonical = canonicalGitHubRepositoryUrl(value);
407
+ if (!canonical) return null;
408
+ const [owner, repo] = new URL(canonical).pathname.split("/").filter(Boolean);
409
+ return { owner, repo, key: `${owner}/${repo}`.toLowerCase() };
410
+ }
411
+
412
+ function resolvePaperDeclaredRepositoryCandidate(candidates) {
413
+ if (candidates.length === 1) return candidates[0];
414
+ if (candidates.length === 0) return null;
415
+ const ranked = [...candidates].sort((left, right) => right.score - left.score);
416
+ const [first, second] = ranked;
417
+ return first.score >= 6 && first.score - second.score >= 4 ? first : null;
418
+ }
419
+
420
+ function repositoryRoleEvidence(context) {
421
+ const normalized = context.replaceAll(/\s+/g, " ").toLowerCase();
422
+ let score = 0;
423
+ const signals = [];
424
+ const apply = (pattern, weight, signal) => {
425
+ if (!pattern.test(normalized)) return;
426
+ score += weight;
427
+ signals.push(signal);
428
+ };
429
+ apply(/reproducibility.{0,120}(?:public release|release|files|code|weights|scripts)/, 8, "reproducibility-release");
430
+ apply(/(?:we|authors?)\s+(?:publicly\s+)?release.{0,160}(?:files|code|weights|scripts|implementation)/, 8, "author-release");
431
+ apply(/scripts?\s+(?:needed|required)\s+to\s+reproduce/, 8, "reproduction-scripts");
432
+ apply(/(?:official|our)\s+(?:code|implementation|repository)/, 7, "official-code");
433
+ apply(/(?:source\s+code|code|implementation)\s+(?:is\s+)?(?:available|released|at|:)/, 6, "code-availability");
434
+ apply(/(?:complete|full)\s+experiment\s+log/, 4, "experiment-log-release");
435
+ apply(/(?:following|based\s+on|adopt(?:ing)?|uses?|using)\s+\[[^\]]*\]?[^.]{0,80}/, -5, "referenced-method");
436
+ apply(/(?:baseline|prior\s+work|dependency|third[- ]party|library|reference\s+implementation)\s+(?:code|repository|implementation)|(?:code|repository|implementation)\s+(?:for|from)\s+(?:the\s+)?(?:baseline|prior\s+work|dependency|third[- ]party)/, -5, "non-primary-code");
437
+ return { score, signals };
438
+ }
439
+
440
+ function githubEvidenceSearchQuery(title, paperText) {
441
+ const seed = typeof title === "string" && title.trim()
442
+ ? title
443
+ : paperText.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length >= 30);
444
+ if (!seed) return null;
445
+ const words = seed
446
+ .replaceAll(/[^a-zA-Z0-9]+/g, " ")
447
+ .split(/\s+/)
448
+ .filter((word) => word.length >= 4)
449
+ .slice(0, 7);
450
+ if (words.length < 4) return null;
451
+ return `"${words.slice(0, 4).join(" ")}" ${words.slice(4).join(" ")} in:readme`;
452
+ }
453
+
454
+ function normalizeIdentityText(value) {
455
+ return typeof value === "string"
456
+ ? value.normalize("NFKD").toLowerCase().replaceAll(/[^a-z0-9]+/g, "")
457
+ : "";
458
+ }
459
+
460
+ function paperTitleCandidates(title, paperText) {
461
+ const lines = paperText
462
+ .slice(0, 12_000)
463
+ .split(/\r?\n/)
464
+ .map((line) => line.trim())
465
+ .filter((line) => line.length >= 12 && line.length <= 220)
466
+ .slice(0, 40);
467
+ const candidates = [];
468
+ for (let index = 0; index < lines.length; index += 1) {
469
+ for (let width = 1; width <= 3 && index + width <= lines.length; width += 1) {
470
+ candidates.push(lines.slice(index, index + width).join(" "));
471
+ }
472
+ }
473
+ if (
474
+ typeof title === "string" &&
475
+ title.trim() &&
476
+ candidates.some((candidate) => titleIdentitySimilarity(title, candidate) >= 0.9)
477
+ ) {
478
+ candidates.push(title);
479
+ }
480
+ return candidates;
481
+ }
482
+
483
+ function titleIdentitySimilarity(left, right) {
484
+ const leftTokens = titleIdentityTokens(left);
485
+ const rightTokens = titleIdentityTokens(right);
486
+ if (leftTokens.size < 7 || rightTokens.size < 7) return 0;
487
+ let shared = 0;
488
+ for (const token of leftTokens) {
489
+ if (rightTokens.has(token)) shared += 1;
490
+ }
491
+ return (2 * shared) / (leftTokens.size + rightTokens.size);
492
+ }
493
+
494
+ function titleIdentityTokens(value) {
495
+ const expanded = String(value ?? "")
496
+ .normalize("NFKD")
497
+ .toLowerCase()
498
+ .replace(/\br\s+wks?\b/g, " random walk kernel ")
499
+ .replace(/\brwks?\b/g, " random walk kernel ");
500
+ const stopWords = new Set(["a", "an", "and", "for", "of", "on", "the", "to"]);
501
+ return new Set(
502
+ expanded
503
+ .split(/[^a-z0-9]+/)
504
+ .filter((token) => token.length >= 3 && !stopWords.has(token))
505
+ .map((token) => token.length > 4 && token.endsWith("s") ? token.slice(0, -1) : token),
506
+ );
507
+ }
508
+
509
+ function arxivId(value) {
510
+ if (typeof value !== "string") return null;
511
+ return value.match(/(?:arxiv(?:\.org\/(?:abs|pdf)\/|\s*[:=]\s*)?)(\d{4}\.\d{4,5})(?:v\d+)?/i)?.[1]
512
+ ?? value.match(/\b(\d{4}\.\d{4,5})(?:v\d+)?\b/)?.[1]
513
+ ?? null;
514
+ }
515
+
516
+ function decodeGitHubContent(value) {
517
+ if (typeof value?.content !== "string" || value.encoding !== "base64") return "";
518
+ return Buffer.from(value.content.replaceAll(/\s+/g, ""), "base64").toString("utf8");
519
+ }
520
+
521
+ async function extractPdfText(paperPath) {
522
+ // Reading-order extraction keeps wrapped links adjacent. Layout mode can
523
+ // interleave the other column between `github.com/` and `owner/repo`, which
524
+ // makes an explicit paper-declared repository impossible to recover safely.
525
+ const result = await captureProcess("pdftotext", ["-nopgbrk", paperPath, "-"]);
526
+ if (result.code !== 0) {
527
+ throw new Error(result.stderr.trim() || `pdftotext exited with ${result.code}`);
528
+ }
529
+ return result.stdout.slice(0, MAX_DOCUMENT_BYTES);
530
+ }
531
+
532
+ async function requestJson(fetchImpl, url, options) {
533
+ const response = await request(fetchImpl, url, options);
534
+ return response.json();
535
+ }
536
+
537
+ async function request(fetchImpl, url, {
538
+ sleep,
539
+ now = () => new Date(),
540
+ observe = () => {},
541
+ service = "external",
542
+ operation = "request",
543
+ headers = {},
544
+ }) {
545
+ let lastError;
546
+ const maxAttempts = 3;
547
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
548
+ const startedAt = now().toISOString();
549
+ let response;
550
+ try {
551
+ response = await fetchImpl(url, {
552
+ headers: {
553
+ accept: "application/vnd.github+json, text/html;q=0.9",
554
+ "user-agent": "CiteArk-Source-Discovery/0.1",
555
+ "x-github-api-version": "2022-11-28",
556
+ ...headers,
557
+ },
558
+ redirect: "follow",
559
+ signal: AbortSignal.timeout(20_000),
560
+ });
561
+ } catch (error) {
562
+ lastError = error;
563
+ recordObservation(observe, externalRequestObservation({
564
+ component: "source_discovery",
565
+ service,
566
+ operation,
567
+ method: "GET",
568
+ url,
569
+ attempt,
570
+ maxAttempts,
571
+ startedAt,
572
+ finishedAt: now().toISOString(),
573
+ error,
574
+ retryable: true,
575
+ }));
576
+ if (attempt < maxAttempts) await sleep(250 * 2 ** (attempt - 1));
577
+ continue;
578
+ }
579
+ const retryable = retryableResponse(response, service);
580
+ const responseError = response.ok ? null : requestError(response, retryable);
581
+ recordObservation(observe, externalRequestObservation({
582
+ component: "source_discovery",
583
+ service,
584
+ operation,
585
+ method: "GET",
586
+ url,
587
+ attempt,
588
+ maxAttempts,
589
+ startedAt,
590
+ finishedAt: now().toISOString(),
591
+ response,
592
+ error: responseError,
593
+ retryable,
594
+ }));
595
+ if (response.ok) return response;
596
+ lastError = responseError;
597
+ if (!retryable || attempt >= maxAttempts) break;
598
+ await sleep(retryDelayMilliseconds(response, attempt));
599
+ }
600
+ throw lastError ?? new Error("联网请求失败");
601
+ }
602
+
603
+ function requestError(response, retryable) {
604
+ const error = new Error(`HTTP ${response.status}`);
605
+ error.name = "ExternalRequestError";
606
+ error.status = response.status;
607
+ error.retryable = retryable;
608
+ return error;
609
+ }
610
+
611
+ function retryableResponse(response, service) {
612
+ if (new Set([408, 425, 429]).has(response.status) || response.status >= 500) return true;
613
+ // 对公开 GitHub 资源而言,403 既可能是主限额,也可能是未携带标准
614
+ // 限流头的次级限流/滥用保护。两类都属于暂时性基础设施失败,不能据此
615
+ // 降级成“论文没有官方仓库”。
616
+ return service === "github" && response.status === 403;
617
+ }
618
+
619
+ function retryDelayMilliseconds(response, attempt) {
620
+ const retryAfter = response.headers.get("retry-after");
621
+ if (retryAfter) {
622
+ const seconds = Number(retryAfter);
623
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.min(30_000, seconds * 1_000);
624
+ }
625
+ const resetSeconds = Number(response.headers.get("x-ratelimit-reset"));
626
+ if (Number.isFinite(resetSeconds) && resetSeconds > 0) {
627
+ return Math.max(0, Math.min(30_000, resetSeconds * 1_000 - Date.now()));
628
+ }
629
+ return 250 * 2 ** (attempt - 1);
630
+ }
631
+
632
+ function recordObservation(observe, observation) {
633
+ try {
634
+ observe(observation);
635
+ } catch {
636
+ // 日志写入失败不得伪装成上游请求失败。
637
+ }
638
+ }
639
+
640
+ function errorMessage(error) {
641
+ return error instanceof Error ? error.message : String(error);
642
+ }
643
+
644
+ function defaultSleep(milliseconds) {
645
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
646
+ }