@customize-agent/server 4.59.2 → 4.60.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 (235) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +52 -31
  3. package/.next/prerender-manifest.json +3 -3
  4. package/.next/required-server-files.json +5 -2
  5. package/.next/routes-manifest.json +22 -0
  6. package/.next/server/chunks/1001.js +2 -56
  7. package/.next/server/chunks/1243.js +3 -0
  8. package/.next/server/chunks/1928.js +1 -1
  9. package/.next/server/chunks/1936.js +2 -2
  10. package/.next/server/chunks/2328.js +6 -0
  11. package/.next/server/chunks/2434.js +4 -4
  12. package/.next/server/chunks/3806.js +1 -0
  13. package/.next/server/chunks/4974.js +2 -0
  14. package/.next/server/chunks/5304.js +2 -0
  15. package/.next/server/chunks/5746.js +5 -0
  16. package/.next/server/chunks/6321.js +1 -3
  17. package/.next/server/chunks/7848.js +141 -0
  18. package/.next/server/chunks/8204.js +1 -1
  19. package/.next/server/chunks/9666.js +141 -0
  20. package/.next/server/instrumentation.js +1 -0
  21. package/.next/server/instrumentation.js.nft.json +1 -0
  22. package/.next/server/middleware-build-manifest.js +1 -1
  23. package/.next/server/pages/404.html +1 -1
  24. package/.next/server/pages/500.html +1 -1
  25. package/.next/server/pages/api/assets/generated/preview.js +1 -1
  26. package/.next/server/pages/api/assets/generated/preview.js.nft.json +1 -1
  27. package/.next/server/pages/api/assets/generated.js +1 -1
  28. package/.next/server/pages/api/assets/generated.js.nft.json +1 -1
  29. package/.next/server/pages/api/config/embedding/healthCheck.js +2 -1
  30. package/.next/server/pages/api/config/embedding/healthCheck.js.nft.json +1 -1
  31. package/.next/server/pages/api/config/embedding.js +2 -1
  32. package/.next/server/pages/api/config/embedding.js.nft.json +1 -1
  33. package/.next/server/pages/api/config/healthCheck.js +2 -2
  34. package/.next/server/pages/api/config/healthCheck.js.nft.json +1 -1
  35. package/.next/server/pages/api/config/models.js +2 -1
  36. package/.next/server/pages/api/config/models.js.nft.json +1 -1
  37. package/.next/server/pages/api/config/providers/[provider].js +2 -1
  38. package/.next/server/pages/api/config/providers/[provider].js.nft.json +1 -1
  39. package/.next/server/pages/api/config/providers.js +2 -2
  40. package/.next/server/pages/api/config/providers.js.nft.json +1 -1
  41. package/.next/server/pages/api/config/web-access.js +2 -1
  42. package/.next/server/pages/api/config/web-access.js.nft.json +1 -1
  43. package/.next/server/pages/api/documents/canvas/[id].js +1 -2
  44. package/.next/server/pages/api/documents/canvas/[id].js.nft.json +1 -1
  45. package/.next/server/pages/api/documents/export.js +11 -9
  46. package/.next/server/pages/api/documents/export.js.nft.json +1 -1
  47. package/.next/server/pages/api/documents/generate/preview.js +1 -2
  48. package/.next/server/pages/api/documents/generate/preview.js.nft.json +1 -1
  49. package/.next/server/pages/api/documents/generate.js +1 -2
  50. package/.next/server/pages/api/documents/generate.js.nft.json +1 -1
  51. package/.next/server/pages/api/documents/generated/[id]/events.js +2 -3
  52. package/.next/server/pages/api/documents/generated/[id]/events.js.nft.json +1 -1
  53. package/.next/server/pages/api/documents/generated/[id]/figure/[name].js +1 -1
  54. package/.next/server/pages/api/documents/generated/[id]/figure/[name].js.nft.json +1 -1
  55. package/.next/server/pages/api/documents/generated/[id]/instruction.js +2 -0
  56. package/.next/server/pages/api/documents/generated/[id]/instruction.js.nft.json +1 -0
  57. package/.next/server/pages/api/documents/generated/[id]/nodes/[nodeId].js +1 -2
  58. package/.next/server/pages/api/documents/generated/[id]/nodes/[nodeId].js.nft.json +1 -1
  59. package/.next/server/pages/api/documents/generated/[id]/nodes.js +1 -2
  60. package/.next/server/pages/api/documents/generated/[id]/nodes.js.nft.json +1 -1
  61. package/.next/server/pages/api/documents/generated/[id]/pause.js +1 -2
  62. package/.next/server/pages/api/documents/generated/[id]/pause.js.nft.json +1 -1
  63. package/.next/server/pages/api/documents/generated/[id]/preview.js +1 -2
  64. package/.next/server/pages/api/documents/generated/[id]/preview.js.nft.json +1 -1
  65. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/confirm.js +1 -1
  66. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/confirm.js.nft.json +1 -1
  67. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/rollback.js +1 -1
  68. package/.next/server/pages/api/documents/generated/[id]/repair/[rid]/rollback.js.nft.json +1 -1
  69. package/.next/server/pages/api/documents/generated/[id]/repair.js +1 -1
  70. package/.next/server/pages/api/documents/generated/[id]/repair.js.nft.json +1 -1
  71. package/.next/server/pages/api/documents/generated/[id]/report.js +1 -0
  72. package/.next/server/pages/api/documents/generated/[id]/report.js.nft.json +1 -0
  73. package/.next/server/pages/api/documents/generated/[id]/resume-generate.js +1 -2
  74. package/.next/server/pages/api/documents/generated/[id]/resume-generate.js.nft.json +1 -1
  75. package/.next/server/pages/api/documents/generated/[id].js +1 -2
  76. package/.next/server/pages/api/documents/generated/[id].js.nft.json +1 -1
  77. package/.next/server/pages/api/documents/generated.js +1 -1
  78. package/.next/server/pages/api/documents/generated.js.nft.json +1 -1
  79. package/.next/server/pages/api/health.js +1 -1
  80. package/.next/server/pages/api/jobs/[id].js +2 -5
  81. package/.next/server/pages/api/jobs/[id].js.nft.json +1 -1
  82. package/.next/server/pages/api/kb/categories.js +1 -4
  83. package/.next/server/pages/api/kb/categories.js.nft.json +1 -1
  84. package/.next/server/pages/api/kb/duplicates.js +1 -4
  85. package/.next/server/pages/api/kb/duplicates.js.nft.json +1 -1
  86. package/.next/server/pages/api/kb/features.js +2 -1
  87. package/.next/server/pages/api/kb/features.js.nft.json +1 -1
  88. package/.next/server/pages/api/kb/files/detail.js +1 -6
  89. package/.next/server/pages/api/kb/files/detail.js.nft.json +1 -1
  90. package/.next/server/pages/api/kb/files/export.js +1 -5
  91. package/.next/server/pages/api/kb/files/export.js.nft.json +1 -1
  92. package/.next/server/pages/api/kb/files/open.js +1 -4
  93. package/.next/server/pages/api/kb/files/open.js.nft.json +1 -1
  94. package/.next/server/pages/api/kb/files/preview-pdf-page.js +1 -4
  95. package/.next/server/pages/api/kb/files/preview-pdf-page.js.nft.json +1 -1
  96. package/.next/server/pages/api/kb/files/reindex.js +2 -1
  97. package/.next/server/pages/api/kb/files/reindex.js.nft.json +1 -1
  98. package/.next/server/pages/api/kb/files/search.js +1 -5
  99. package/.next/server/pages/api/kb/files/search.js.nft.json +1 -1
  100. package/.next/server/pages/api/kb/files/source-preview.js +1 -0
  101. package/.next/server/pages/api/kb/files/source-preview.js.nft.json +1 -0
  102. package/.next/server/pages/api/kb/files.js +2 -6
  103. package/.next/server/pages/api/kb/files.js.nft.json +1 -1
  104. package/.next/server/pages/api/kb/ignore.js +1 -4
  105. package/.next/server/pages/api/kb/ignore.js.nft.json +1 -1
  106. package/.next/server/pages/api/kb/material-groups.js +1 -2
  107. package/.next/server/pages/api/kb/material-groups.js.nft.json +1 -1
  108. package/.next/server/pages/api/kb/operations.js +2 -5
  109. package/.next/server/pages/api/kb/operations.js.nft.json +1 -1
  110. package/.next/server/pages/api/kb/reindex.js +2 -1
  111. package/.next/server/pages/api/kb/reindex.js.nft.json +1 -1
  112. package/.next/server/pages/api/kb/stats.js +1 -4
  113. package/.next/server/pages/api/kb/stats.js.nft.json +1 -1
  114. package/.next/server/pages/api/kb/tags.js +1 -4
  115. package/.next/server/pages/api/kb/tags.js.nft.json +1 -1
  116. package/.next/server/pages/api/kb/upload/complete.js +2 -1
  117. package/.next/server/pages/api/kb/upload/complete.js.nft.json +1 -1
  118. package/.next/server/pages/api/kb/upload.js +2 -1
  119. package/.next/server/pages/api/kb/upload.js.nft.json +1 -1
  120. package/.next/server/pages/api/prompt/chat.js +5 -11
  121. package/.next/server/pages/api/prompt/chat.js.nft.json +1 -1
  122. package/.next/server/pages/api/prompt.js +2 -1
  123. package/.next/server/pages/api/prompt.js.nft.json +1 -1
  124. package/.next/server/pages/api/skills/[id].js +1 -1
  125. package/.next/server/pages/api/skills/[id].js.nft.json +1 -1
  126. package/.next/server/pages/api/skills/validate.js +1 -1
  127. package/.next/server/pages/api/skills/validate.js.nft.json +1 -1
  128. package/.next/server/pages/api/skills.js +1 -1
  129. package/.next/server/pages/api/skills.js.nft.json +1 -1
  130. package/.next/server/pages/api/system/data-health.js +2 -2
  131. package/.next/server/pages/api/system/data-health.js.nft.json +1 -1
  132. package/.next/server/pages/api/system/logs.js +2 -2
  133. package/.next/server/pages/api/system/logs.js.nft.json +1 -1
  134. package/.next/server/pages/api/system/reset.js +1 -0
  135. package/.next/server/pages/api/{kb/intelligence.js.nft.json → system/reset.js.nft.json} +1 -1
  136. package/.next/server/pages/api/system/stats.js +2 -1
  137. package/.next/server/pages/api/system/stats.js.nft.json +1 -1
  138. package/.next/server/pages/canvas/[id].html +1 -1
  139. package/.next/server/pages/canvas.html +1 -1
  140. package/.next/server/pages/documents.html +1 -1
  141. package/.next/server/pages/guide.html +1 -1
  142. package/.next/server/pages/index.html +1 -1
  143. package/.next/server/pages/knowledge/file-detail.html +1 -1
  144. package/.next/server/pages/knowledge/files.html +1 -1
  145. package/.next/server/pages/knowledge/manage.html +1 -1
  146. package/.next/server/pages/knowledge/source-preview.html +1 -0
  147. package/.next/server/pages/knowledge/source-preview.js.nft.json +1 -0
  148. package/.next/server/pages/knowledge.html +1 -1
  149. package/.next/server/pages/models.html +1 -1
  150. package/.next/server/pages/overview.html +1 -1
  151. package/.next/server/pages/prompt.html +1 -1
  152. package/.next/server/pages/settings.html +1 -1
  153. package/.next/server/pages/skills.html +1 -1
  154. package/.next/server/pages/system/logs.html +1 -1
  155. package/.next/server/pages-manifest.json +36 -37
  156. package/.next/static/77d815e00c04639e13f00085615df788b0e214c81a14c8f0ba89cbbe491ff7c3/_buildManifest.js +1 -0
  157. package/.next/static/chunks/278-31fbe7e1b733a9b7.js +1 -0
  158. package/.next/static/chunks/{426-90638907336adcb8.js → 426-22c4cedb04b0d45d.js} +1 -1
  159. package/.next/static/chunks/511-75b881093a3e908b.js +1 -0
  160. package/.next/static/chunks/530-d9f0418ec7ab040f.js +1 -0
  161. package/.next/static/chunks/665-9607db8f8b43e52e.js +1 -0
  162. package/.next/static/chunks/795-48065c56c3b80339.js +1 -0
  163. package/.next/static/chunks/823-ff35afb174f7998b.js +1 -0
  164. package/.next/static/chunks/835-d29f4cc7a3679606.js +1 -0
  165. package/.next/static/chunks/864-b06700e1a4e573f2.js +6 -0
  166. package/.next/static/chunks/873-16d052cdd3768758.js +1 -0
  167. package/.next/static/chunks/964-7e9fe2d80a53aeee.js +78 -0
  168. package/.next/static/chunks/992-8032f477230baaaf.js +1 -0
  169. package/.next/static/chunks/pages/{_app-c560816f0a8a0815.js → _app-c11b5e8072f49bcc.js} +20 -20
  170. package/.next/static/chunks/pages/canvas/[id]-37bee3f7b816ed18.js +1 -0
  171. package/.next/static/chunks/pages/canvas-97971ae159337676.js +1 -0
  172. package/.next/static/chunks/pages/guide-a8eecc6c02b5bb9c.js +1 -0
  173. package/.next/static/chunks/pages/knowledge/file-detail-f89e6044f752277c.js +1 -0
  174. package/.next/static/chunks/pages/knowledge/source-preview-4a303fadb123430a.js +1 -0
  175. package/.next/static/chunks/pages/knowledge-a65ec687334ffed0.js +1 -0
  176. package/.next/static/chunks/pages/models-3e486e49cca91e5a.js +1 -0
  177. package/.next/static/chunks/pages/prompt-b8af2cef904e3e19.js +1 -0
  178. package/.next/static/chunks/pages/settings-81dffd92939c1d36.js +1 -0
  179. package/.next/static/chunks/pages/skills-77ba363270239bbf.js +1 -0
  180. package/.next/static/chunks/pages/system/logs-1f54b6c8791daa77.js +1 -0
  181. package/.next/static/css/1a59bfb0aa677e69.css +1 -0
  182. package/README.md +48 -45
  183. package/messages/en-US.json +76 -65
  184. package/messages/zh-CN.json +76 -65
  185. package/next.config.ts +5 -0
  186. package/package.json +7 -9
  187. package/public/fonts/NotoSansCJKsc-Regular.otf +0 -0
  188. package/public/fonts/OFL-Noto-CJK.txt +142 -0
  189. package/public/fonts/font-manifest.json +8 -0
  190. package/scripts/build-identity.cjs +67 -0
  191. package/scripts/capture-baseline.mjs +100 -290
  192. package/scripts/check-workspace-deps.cjs +141 -0
  193. package/scripts/half-built-scan.allow.json +142 -0
  194. package/scripts/half-built-scan.baseline.json +206 -0
  195. package/scripts/half-built-scan.mjs +275 -55
  196. package/scripts/kb-fingerprint-worker.cjs +28 -0
  197. package/scripts/kb-index-worker.cjs +27 -20
  198. package/scripts/kb-worker-ipc.cjs +34 -0
  199. package/scripts/purge-next-artifacts.cjs +70 -0
  200. package/scripts/verify-next-static.js +2 -2
  201. package/.next/server/chunks/299.js +0 -50
  202. package/.next/server/chunks/4435.js +0 -350
  203. package/.next/server/chunks/6087.js +0 -3
  204. package/.next/server/pages/api/jobs.js +0 -5
  205. package/.next/server/pages/api/jobs.js.nft.json +0 -1
  206. package/.next/server/pages/api/kb/evaluate.js +0 -10
  207. package/.next/server/pages/api/kb/evaluate.js.nft.json +0 -1
  208. package/.next/server/pages/api/kb/files/tree.js +0 -5
  209. package/.next/server/pages/api/kb/files/tree.js.nft.json +0 -1
  210. package/.next/server/pages/api/kb/intelligence.js +0 -1
  211. package/.next/server/pages/api/kb/search.js +0 -4
  212. package/.next/server/pages/api/kb/search.js.nft.json +0 -1
  213. package/.next/server/pages/api/kb/upload/progress.js +0 -5
  214. package/.next/server/pages/api/kb/upload/progress.js.nft.json +0 -1
  215. package/.next/static/2MjosIoMmZUcFnY_P02ZD/_buildManifest.js +0 -1
  216. package/.next/static/chunks/243-03eba51d5d737e98.js +0 -1
  217. package/.next/static/chunks/289-136ebb30e0f6723a.js +0 -78
  218. package/.next/static/chunks/422-46f07037c58d2309.js +0 -1
  219. package/.next/static/chunks/445-d12731f107978b10.js +0 -6
  220. package/.next/static/chunks/506-93f730bc149d0dce.js +0 -1
  221. package/.next/static/chunks/530-df2ce692cf0fa512.js +0 -1
  222. package/.next/static/chunks/795-81a3e3973fc43791.js +0 -1
  223. package/.next/static/chunks/850-0f48354694fc6b3e.js +0 -1
  224. package/.next/static/chunks/pages/canvas/[id]-515edd3e72acaf82.js +0 -1
  225. package/.next/static/chunks/pages/canvas-323d44fe56d685a7.js +0 -1
  226. package/.next/static/chunks/pages/guide-7367305082617e3c.js +0 -1
  227. package/.next/static/chunks/pages/knowledge/file-detail-aee7c15541d1d130.js +0 -1
  228. package/.next/static/chunks/pages/knowledge-ca8ad5501faed486.js +0 -1
  229. package/.next/static/chunks/pages/models-8b96ba85131d5408.js +0 -1
  230. package/.next/static/chunks/pages/prompt-dd9a9f9105216e2b.js +0 -1
  231. package/.next/static/chunks/pages/settings-0a7709bf039a2a4d.js +0 -1
  232. package/.next/static/chunks/pages/skills-3d77213c3e9ef0b5.js +0 -1
  233. package/.next/static/chunks/pages/system/logs-9deb1217739490af.js +0 -1
  234. package/.next/static/css/85c434f8fd3a17ae.css +0 -1
  235. /package/.next/static/{2MjosIoMmZUcFnY_P02ZD → 77d815e00c04639e13f00085615df788b0e214c81a14c8f0ba89cbbe491ff7c3}/_ssgManifest.js +0 -0
@@ -1,35 +1,9 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * 半成品扫描器(2026-09-28)。
4
- *
5
- * ## 为什么要有它
6
- * 用户口径:「每次问你都会遗漏很多东西,都会有很多都是半成品」。
7
- * 结构性原因不是"不够仔细",而是:**仓库里已有九份缺口账本,散在九个文件里,没有一处汇总**——
8
- * 所以任何一次凭记忆回答都必然只覆盖一两份(见 `docs/half-built-inventory-2026-09-28.md` 第零节)。
9
- *
10
- * 本脚本负责的是**账本记不住的那一类**:**声明了、实现了,但生产代码里零调用点**。
11
- *
12
- * ## 判定口径(刻意保守)
13
- * 对每个导出符号分别统计:
14
- * - `inFile`:**本文件内**引用次数(扣掉声明行本身)——>0 说明它是模块内部实现的一部分(正常);
15
- * - `otherProd`:**其它生产文件**引用次数——**这是唯一判定"接线与否"的量**;
16
- * - `tests`:测试目录引用次数——>0 且 otherProd=0 = **典型的"只在测试里活着"**。
17
- *
18
- * 三种分类:
19
- * | 分类 | 判据 | 含义 |
20
- * |---|---|---|
21
- * | 半成品嫌疑 | otherProd=0 且 tests>0 | 写好了、测过了,生产里没人用 |
22
- * | 谁都不用 | inFile=0 且 otherProd=0 且 tests=0 | 真死代码,或被取代的旧 API |
23
- * | 封装暴露 | otherProd=0 但 inFile>0 | 只是导出给测试用,**不是**半成品 |
24
- *
25
- * ## 已知局限(写在这里,免得被当成结论用)
26
- * - **动态引用识别不了**:`await import('...')`、字符串派发、计算属性访问都会漏判 ⇒ 会误报;
27
- * - 因此本脚本的输出是**候选清单**,逐条必须 grep 复核后才能写进账本;
28
- * 实测教训:本轮用它测"画布标签表",头两版都测错(表其实是完整的),最后以读文件逐键核对为准。
29
- *
30
- * ## 用法
31
- * node apps/server/scripts/half-built-scan.mjs # 全仓
32
- * node apps/server/scripts/half-built-scan.mjs apps/server/src/services # 指定目录
2
+ /** 文本引用候选扫描,覆盖源码、测试和脚本,供实际调用闭包审查使用。
3
+ * 新增零引用导出、未处置的仅测试引用与星号转导出候选均阻断;其他增量显式报告。
4
+ * 转导出、动态派发、类型引用与副作用需独立核查,不能据文本数量删除实际入口。
5
+ * 基线只收紧已退休条目;登记仅允许有具体用途的观察/测试面,不能掩盖未接入业务。
6
+ * 用法:node apps/server/scripts/half-built-scan.mjs --full --gate --baseline <file>
33
7
  */
34
8
  import fs from 'node:fs';
35
9
  import path from 'node:path';
@@ -38,10 +12,56 @@ import { fileURLToPath } from 'node:url';
38
12
  const HERE = path.dirname(fileURLToPath(import.meta.url));
39
13
  const ROOT = path.resolve(HERE, '../../..'); // 仓库根
40
14
  const DEFAULT_DIRS = ['apps/server/src', 'packages'];
41
- const DIRS = process.argv.slice(2).length > 0 ? process.argv.slice(2) : DEFAULT_DIRS;
15
+ const ARGS = process.argv.slice(2);
16
+ /**
17
+ * ★ D1(2026-09-28):`--full` 打印**全量**清单(默认只列前 30 条)。
18
+ * 批量清理时要的是完整名单("列前 30" 只够读一眼,不够动手)。
19
+ */
20
+ const FULL = ARGS.includes('--full');
21
+ /**
22
+ * ★ G2(2026-09-29):**带值的开关必须把它的值一起从位置参数里摘掉**。
23
+ *
24
+ * 旧实现 `ARGS.filter(arg => !arg.startsWith('--'))` 只滤旗标 ⇒ `--baseline foo.json`
25
+ * 会把 `foo.json` 当成"扫描目录"传进 `DIRS`,于是报告范围变成"仓库根下的 foo.json"
26
+ * (**静默空扫**:所有计数归零,门禁恒绿)。这正是本脚本注释里反复出现的同一类错:
27
+ * 工具安静地给出错误结论比不给结论更糟。
28
+ */
29
+ const VALUE_FLAGS = new Set(['--baseline', '--write-baseline']);
30
+ const POSITIONAL = [];
31
+ for (let i = 0; i < ARGS.length; i++) {
32
+ if (VALUE_FLAGS.has(ARGS[i])) {
33
+ // ★ 只吞"真像值"的下一个参数:`--write-baseline --gate` 里 `--gate` 是旗标,
34
+ // 吞掉它等于把这个开关静默吃掉(同"静默空扫"一类错)。
35
+ if (ARGS[i + 1] !== undefined && !ARGS[i + 1].startsWith('--')) i++;
36
+ continue;
37
+ }
38
+ if (ARGS[i].startsWith('--')) continue;
39
+ POSITIONAL.push(ARGS[i]);
40
+ }
41
+ const DIRS = POSITIONAL.length > 0 ? POSITIONAL : DEFAULT_DIRS;
42
+
43
+ const GATE = ARGS.includes('--gate');
44
+ const flagValue = name => {
45
+ const i = ARGS.indexOf(name);
46
+ return i >= 0 ? ARGS[i + 1] : undefined;
47
+ };
48
+ const BASELINE_ARG = flagValue('--baseline');
49
+ const WRITE_BASELINE_ARG = flagValue('--write-baseline');
50
+ /** 默认基线路径:与脚本同目录(CI 与本地同一条路径) */
51
+ const DEFAULT_BASELINE = path.join(HERE, 'half-built-scan.baseline.json');
42
52
 
43
53
  const TEST_HINT = /(^|\/)(test|tests|__tests__|__mocks__)\//;
54
+ /** 报告范围:只报 `.ts`/`.tsx` 的导出符号 */
44
55
  const isSource = file => /\.(ts|tsx)$/.test(file) && !/\.d\.ts$/.test(file);
56
+ /**
57
+ * ★ 2026-09-29:**引用池要收 `.cjs`/`.mjs`/`.js`**(此前只收 `.ts/.tsx`,是最后一个已知假阳性源)。
58
+ *
59
+ * 实测代价:`searchKb`(`lib/api.ts`)只被 `scripts/upload-matrix.cjs` 用着,却因 `.cjs` 不在池里
60
+ * 被判成"谁都不用"——`gitignore` 级的工具盲区会让清理任务删掉在用的代码。
61
+ * 与"D1 把测试目录收进池子"是同一类修正:**引用池必须覆盖全部消费者**。
62
+ * (报告范围仍是 `.ts/.tsx`:`.cjs/.mjs` 不进清单,只当引用来源。)
63
+ */
64
+ const isPoolFile = file => /\.(ts|tsx|cjs|mjs|js)$/.test(file) && !/\.d\.ts$/.test(file);
45
65
  const SKIP_DIRS = new Set(['node_modules', 'dist', '.next', 'coverage']);
46
66
 
47
67
  function walk(dir, out = []) {
@@ -51,7 +71,7 @@ function walk(dir, out = []) {
51
71
  if (entry.isDirectory()) {
52
72
  if (SKIP_DIRS.has(entry.name)) continue;
53
73
  walk(full, out);
54
- } else if (isSource(full)) out.push(full);
74
+ } else if (isPoolFile(full)) out.push(full);
55
75
  }
56
76
  return out;
57
77
  }
@@ -63,13 +83,68 @@ function walk(dir, out = []) {
63
83
  * `document-workflow` 的 `closePhaseNode` / `closeDanglingConnections` 被误报成"谁都不用"。
64
84
  * **工具撒谎比没有工具更糟**:这会让人去删正在用的代码。
65
85
  */
66
- const ALL_DIRS = DEFAULT_DIRS;
67
- const allFiles = ALL_DIRS.flatMap(dir => walk(path.resolve(ROOT, dir)));
86
+ /**
87
+ * ★ D1(2026-09-28)修正:**引用池必须覆盖测试与脚本目录**。
88
+ *
89
+ * 原实现 `const ALL_DIRS = DEFAULT_DIRS`(= `['apps/server/src','packages']`)与下面那句
90
+ * 「报告范围 = 参数;**引用池 = 全仓**」的**注释意图相反**:测试目录与脚本不在池子里,
91
+ * 于是 `tests=0` 从来就不是"测试也不用",而是"没查过测试"。
92
+ *
93
+ * 代价实测:一次清理任务按它的"谁都不用(140 条)"逐条真 grep 复核,**133 条被拦下**——
94
+ * 误报率 96%(`getKbTags` 这类"只在测试里活着"的根本没被算进去)。
95
+ * 一个 96% 误报的清单比没有清单更危险:照着它删就是成片删掉在用的东西。
96
+ */
97
+ const REFERENCE_DIRS = ['apps/server/src', 'apps/server/test', 'apps/server/scripts', 'packages', 'scripts'];
98
+
99
+ /**
100
+ * ★ 2026-09-29:**`export *` 再导出的文件不可判**(第二个已知盲区,与 `.cjs` 消费者同类)。
101
+ *
102
+ * `export * from './x'` 让 `x` 的每个符号都可达外部,但**没有任何地方提到那些符号名**——
103
+ * 于是它们会被三分类静默吞掉(既不在 otherProd,也不在 inFile/test),看起来像"封装暴露/谁都不用"。
104
+ * 实测本仓有 14 个这样的文件(`types/index.ts`、`constants/index.ts`、`chapterGeneration.ts` 等)。
105
+ * 后果与"照着 96% 误报清单删代码"同型:**工具撒谎比没有工具更糟**。
106
+ * 处置:这些文件里的符号**不进任何清单**,改为在报告末尾单独列出(人工判),
107
+ * 并在 `--csv` 里标 `verdict=不可判(星号再导出)`。
108
+ */
109
+ function starReExportTargets() {
110
+ const targets = new Set();
111
+ for (const file of allFiles) {
112
+ const text = texts.get(file) ?? '';
113
+ for (const m of text.matchAll(/^\s*export \*\s*(?:as\s+\w+\s*)?from\s*'(\.[^']+)'/gm)) {
114
+ const base = path.resolve(path.dirname(file), m[1]);
115
+ for (const cand of [`${base}.ts`, `${base}.tsx`, path.join(base, 'index.ts')]) {
116
+ if (fs.existsSync(cand)) { targets.add(cand); break; }
117
+ }
118
+ }
119
+ }
120
+ return targets;
121
+ }
122
+ /**
123
+ * ⚠️ **不把 `docs/` 放进引用池**:文档是散文,而且本仓的文档里大量是**缺口清单**
124
+ * ("这条写好没接线")——把清单算成"有引用",会让真死代码因为它被记过一笔而永远不被清理。
125
+ * 文档命中的符号由**人**处置(要么接上、要么连同条目一起删),不交给扫描器自动判。
126
+ * 另一个已知盲区:`.cjs`/`.mjs`/`.js` 消费者(`isSource` 只认 .ts/.tsx)——
127
+ * 实测 `searchKb` 只被 `scripts/upload-matrix.cjs` 用着,曾因此被误判为死代码。
128
+ */
129
+ const ALL_DIRS = REFERENCE_DIRS;
130
+ /**
131
+ * ★ 2026-09-29:**引用池必须剔除本脚本自身**(第三个盲区,且是最隐蔽的一个)。
132
+ *
133
+ * 本脚本的注释里**点名**了一堆过去被判死的符号(`searchKb`、`getKbTags` 那批)——
134
+ * 收进 `.cjs/.mjs` 池之后,脚本读自己,**自己的散文成了"引用"**:`searchKb` 于是从"谁都不用"
135
+ * 消失(假阴性),而它其实真的零调用(`grep -rn searchKb --include=*.cjs` = 0)。
136
+ * **工具把自己算成消费者 = 用自己的注释证明自己没错**。剔除自身后结论才可核。
137
+ */
138
+ const SELF = fileURLToPath(import.meta.url);
139
+ const allFiles = ALL_DIRS.flatMap(dir => walk(path.resolve(ROOT, dir)))
140
+ .filter(file => path.resolve(file) !== path.resolve(SELF));
68
141
  const texts = new Map(allFiles.map(f => [f, fs.readFileSync(f, 'utf8')]));
69
142
  const prod = allFiles.filter(f => !TEST_HINT.test(f));
70
143
  const tests = allFiles.filter(f => TEST_HINT.test(f));
71
144
  const scopeRoots = DIRS.map(dir => path.resolve(ROOT, dir));
72
145
  const inScope = file => scopeRoots.some(root => file.startsWith(root + path.sep) || file === root);
146
+ /** 被 `export *` 再导出的文件(其符号名不会被任何地方提到 ⇒ 不可判,见上文说明) */
147
+ const STAR_REEXPORTED = starReExportTargets();
73
148
 
74
149
  const EXPORT_PATTERNS = [
75
150
  [/^export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/gm, 'fn'],
@@ -106,9 +181,10 @@ for (const file of prod) {
106
181
  if (otherProd > 0) continue;
107
182
  rows.push({
108
183
  rel, name, kind, inFile, testRefs,
109
- verdict: inFile === 0 && testRefs === 0 ? '谁都不用'
110
- : inFile === 0 && testRefs > 0 ? '半成品嫌疑'
111
- : '封装暴露(只导出给测试)',
184
+ verdict: STAR_REEXPORTED.has(file) ? '不可判(星号再导出)'
185
+ : inFile === 0 && testRefs === 0 ? '谁都不用'
186
+ : inFile === 0 && testRefs > 0 ? '半成品嫌疑'
187
+ : '封装暴露(只导出给测试)',
112
188
  });
113
189
  }
114
190
  }
@@ -117,34 +193,178 @@ for (const file of prod) {
117
193
  const byVerdict = verdict => rows.filter(r => r.verdict === verdict);
118
194
  const suspect = byVerdict('半成品嫌疑');
119
195
  const unused = byVerdict('谁都不用');
196
+ const undecidable = byVerdict('不可判(星号再导出)');
197
+
198
+ /**
199
+ * ★ 2026-09-30(用户口径:**这类清单不许推给人**):**登记表**把"设计如此的测试/观察/登记面"
200
+ * 从"半成品嫌疑"里分出来——登记条目**带理由**(`half-built-scan.allow.json`,改一项也要改理由),
201
+ * 不进"未登记"计数;新增的零引用导出会落进**未登记** ⇒ 当场判(接线 / 删除 / 登记)。
202
+ */
203
+ const ALLOW_PATH = path.join(HERE, 'half-built-scan.allow.json');
204
+ const allowed = (() => {
205
+ try {
206
+ const file = JSON.parse(fs.readFileSync(ALLOW_PATH, 'utf8'));
207
+ const map = new Map();
208
+ for (const entry of file.entries || []) map.set(`${entry.file}::${entry.symbol}`, entry.reason);
209
+ return map;
210
+ } catch {
211
+ return new Map();
212
+ }
213
+ })();
214
+ const registered = suspect.filter(row => allowed.has(`${row.rel}::${row.name}`));
215
+ const unregistered = suspect.filter(row => !allowed.has(`${row.rel}::${row.name}`));
216
+ /** ★ 2026-09-30:**"不可判"也进同一张登记表**(星号再导出里既有"本文件内自用"也有测试面——都登记,别留待判) */
217
+ const undecidableRegistered = undecidable.filter(row => allowed.has(`${row.rel}::${row.name}`));
218
+ const undecidableUnregistered = undecidable.filter(row => !allowed.has(`${row.rel}::${row.name}`));
120
219
 
121
220
  console.log('════════ 半成品扫描 ════════');
122
221
  console.log(`扫描范围:${DIRS.join(', ')}(引用池 = 全仓 ${prod.length} 个生产文件 / ${tests.length} 个测试文件)`);
123
222
  console.log(`导出符号在他处生产零引用:${rows.length}`);
124
- console.log(` · 半成品嫌疑(仅测试引用) ${suspect.length}`);
223
+ console.log(` · 半成品嫌疑(仅测试引用) ${suspect.length}(**未登记 ${unregistered.length}** / 已登记 ${registered.length})`);
125
224
  console.log(` · 谁都不用(含自身文件) ${unused.length}`);
126
225
  console.log(` · 封装暴露(本文件内自用) ${byVerdict('封装暴露(只导出给测试)').length}`);
226
+ console.log(` · 不可判(星号再导出) ${undecidable.length}(**未登记 ${undecidableUnregistered.length}** / 已登记 ${undecidableRegistered.length})`);
227
+
228
+ /**
229
+ * ★ 2026-09-29:`--csv` 输出可处理名单(`rel,name,kind,inFile,testRefs,verdict`)。
230
+ * 批量清理要的是**能进表格/能过脚本**的名单,不是给人读的缩进文本。
231
+ */
232
+ if (ARGS.includes('--csv')) {
233
+ console.log('file,name,kind,inFile,testRefs,verdict');
234
+ for (const row of rows) {
235
+ console.log([row.rel, row.name, row.kind, row.inFile, row.testRefs, row.verdict].join(','));
236
+ }
237
+ process.exit(0);
238
+ }
127
239
 
128
240
  const print = (title, list, limit = 40) => {
129
241
  if (list.length === 0) return;
130
- console.log(`\n── ${title}(${list.length} 条,列前 ${Math.min(limit, list.length)})`);
131
- for (const row of list.slice(0, limit)) {
242
+ console.log(`\n── ${title}(${list.length} 条${FULL ? '' : `,列前 ${Math.min(limit, list.length)}`})`);
243
+ for (const row of (FULL ? list : list.slice(0, limit))) {
132
244
  console.log(` ${row.rel} :: ${row.name}${row.testRefs ? ` (测试引用 ${row.testRefs})` : ''}`);
133
245
  }
134
246
  };
135
- print('半成品嫌疑', suspect);
247
+ print('半成品嫌疑 未登记(**要当场判**:接线 / 删除 / 写进登记表并给理由)', unregistered);
248
+ print('半成品嫌疑 已登记(测试/观察/登记面,理由见 half-built-scan.allow.json)', registered);
249
+ if (registered.length > 0 && FULL) {
250
+ for (const row of registered) console.log(` · ${row.rel} :: ${row.name} —— ${allowed.get(`${row.rel}::${row.name}`)}`);
251
+ }
136
252
  print('谁都不用', unused, 30);
253
+ print('不可判(星号再导出:符号可达但没有名字被提到,本脚本判不了)', undecidable, 10);
254
+
255
+ console.log('\n完整整改与实际运行证据见 docs/project-improvements/2026-10-11-status-and-architecture.md。');
256
+ console.log('本扫描仅提供文本引用候选;转导出、动态入口、内部可达依赖和副作用需按实际调用另行核查。');
257
+
258
+ // ═══════════════════════════ 门禁(G2,2026-09-29)═══════════════════════════
259
+ /**
260
+ * 口径全文在文件头「## 门禁口径(G2)」一节。这里只写实现锚点:
261
+ * - **四类计数 + 四类名录**都进基线(名录让"新增了什么"可以逐条打出来,而不是只给一个差值);
262
+ * - **只有「谁都不用」红**;其余三类超基线只 `⚠`(理由:366 条"封装暴露"是刻意形态,
263
+ * 判红 = 每加一个测试用导出就挂 CI = 门禁当天就该被绕过);
264
+ * - 基线文件缺失时「谁都不用」按 **0** 判(零容忍),其余三类不做棘轮比较(没有基线就没有差值)。
265
+ */
266
+ const VERDICT_KEYS = ['谁都不用', '半成品嫌疑', '封装暴露(只导出给测试)', '不可判(星号再导出)'];
267
+ const GATED_KEY = '谁都不用';
268
+ const counts = Object.fromEntries(VERDICT_KEYS.map(key => [key, rows.filter(row => row.verdict === key).length]));
269
+ const roster = Object.fromEntries(VERDICT_KEYS.map(key => [
270
+ key,
271
+ rows.filter(row => row.verdict === key).map(row => `${row.rel}::${row.name}`).sort(),
272
+ ]));
273
+
274
+ /** 基线文件路径:`--baseline <file>` 显式给的优先;`--write-baseline [file]` 同理 */
275
+ const writeBaselineTarget = WRITE_BASELINE_ARG !== undefined && !WRITE_BASELINE_ARG.startsWith('--')
276
+ ? WRITE_BASELINE_ARG
277
+ : DEFAULT_BASELINE;
278
+ const baselinePath = BASELINE_ARG ? path.resolve(process.cwd(), BASELINE_ARG) : DEFAULT_BASELINE;
137
279
 
138
- console.log('\n════════ 另外九份账本(本脚本扫不到,必须一起看)════════');
139
- for (const line of [
140
- 'L1 A/B 接线缺口 test/services/agent/detailLayerWiring.test.ts 的 WIRING_GAPS',
141
- 'L2 N 类判据处置表 document-workflow/nClassJudges.ts 的 N_CLASS_JUDGE_DISPOSITIONS',
142
- 'L3 写作期处置欠账 detectorFixerRegistry.ts 的 PENDING_WRITING_TIME_DISPOSITION',
143
- 'L4 未锚定修复轮 agent/tools/repairRounds.ts 的 unanchoredRepairRounds()',
144
- 'L5 manual 检测器 detectorFixerRegistry 的 fixerDisposition==="manual"(54 条)',
145
- 'L6 工具面空壳(已删) services/agent/toolRegistry.ts 的删除记录',
146
- 'L7 91 条对照表缺口 docs/closure-checklist-mapping-2026-09-28.md',
147
- 'L8 机制级缺口 docs/judgment-completeness-2026-09-28.md',
148
- 'L9 零生产者节点类型 docs/half-built-inventory-2026-09-28.md 第一节',
149
- ]) console.log(` ${line}`);
150
- console.log('\n完整口径与不确定性见 docs/half-built-inventory-2026-09-28.md。');
280
+ if (ARGS.includes('--write-baseline')) {
281
+ const target = path.resolve(process.cwd(), writeBaselineTarget);
282
+ fs.writeFileSync(target, `${JSON.stringify({
283
+ note: '半成品扫描门禁基线(棘轮:数不许变多)。重新生成:node apps/server/scripts/half-built-scan.mjs --write-baseline',
284
+ generatedAt: new Date().toISOString().slice(0, 10),
285
+ scope: DIRS,
286
+ counts,
287
+ roster,
288
+ }, null, 2)}\n`, 'utf8');
289
+ console.log(`\n★ 已写入基线:${path.relative(ROOT, target)}(谁都不用 ${counts[GATED_KEY]} 条)`);
290
+ process.exit(0);
291
+ }
292
+
293
+ if (GATE) {
294
+ let baseline = { counts: {}, roster: {} };
295
+ let baselineNote = '未提供(「谁都不用」按 0 零容忍判;其余三类不做棘轮比较)';
296
+ if (fs.existsSync(baselinePath)) {
297
+ baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
298
+ baselineNote = `${path.relative(ROOT, baselinePath)}(生成于 ${baseline.generatedAt ?? '未知'})`;
299
+ } else if (BASELINE_ARG) {
300
+ // 显式给了路径却不存在 = 大概率是路径写错。**不静默降级**:报出来,免得基线悄悄失效。
301
+ console.log(`\n⚠ 基线文件不存在:${baselinePath}(按「谁都不用 = 0」零容忍口径继续)`);
302
+ }
303
+
304
+ const baselineRoster = key => new Set(Array.isArray(baseline.roster?.[key]) ? baseline.roster[key] : []);
305
+ const currentRoster = key => new Set(roster[key]);
306
+ const grew = key => [...currentRoster(key)].filter(id => !baselineRoster(key).has(id));
307
+ const shrank = key => [...baselineRoster(key)].filter(id => !currentRoster(key).has(id));
308
+
309
+ console.log('\n════════ 门禁判定(--gate)════════');
310
+ console.log(`基线:${baselineNote}`);
311
+ const gatedBaseline = typeof baseline.counts?.[GATED_KEY] === 'number' ? baseline.counts[GATED_KEY] : 0;
312
+ console.log(`【硬红】${GATED_KEY}:${counts[GATED_KEY]} 条(基线允许 ≤ ${gatedBaseline})`);
313
+ for (const key of VERDICT_KEYS) {
314
+ if (key === GATED_KEY) continue;
315
+ const base = baseline.counts?.[key];
316
+ console.log(`【只报】${key}:${counts[key]} 条${typeof base === 'number' ? `(基线 ${base})` : ''}`);
317
+ }
318
+
319
+ const newDead = grew(GATED_KEY);
320
+ const vanishedDead = shrank(GATED_KEY);
321
+ /**
322
+ * ★ **扫描范围与基线不一致 ⇒ 直接红,不比较计数**。
323
+ *
324
+ * 计数只在"同一把尺子"下可比:拿 `apps/server/src/services`(局部)的计数去比全仓基线,
325
+ * 得到的差异全是噪音;反过来更糟——拿局部范围的 0 去"证明"全仓没有死符号。
326
+ * 这正是本脚本注释里那条纪律:**宁可红,不许静默给一个错误结论**。
327
+ */
328
+ const baselineScope = Array.isArray(baseline.scope) ? baseline.scope.join(',') : '';
329
+ const scopeMismatch = baselineScope !== '' && baselineScope !== DIRS.join(',');
330
+ /**
331
+ * ★ 2026-09-30:**未登记的"半成品嫌疑"同样硬红**——登记表(allow.json)之外新出现的零引用导出
332
+ * 必须当场判(接线 / 删除 / 写进登记表并给理由),否则这份清单又会退化成"堆给人看"的待办。
333
+ */
334
+ let failed = scopeMismatch || counts[GATED_KEY] > gatedBaseline || newDead.length > 0 || unregistered.length > 0 || undecidableUnregistered.length > 0;
335
+ if (scopeMismatch) {
336
+ console.log(`\n❌ 门禁红:扫描范围与基线不一致,计数不可比`);
337
+ console.log(` 基线:${baselineScope}`);
338
+ console.log(` 本次:${DIRS.join(',')}`);
339
+ console.log(' 要门禁局部范围,请先用同一范围重跑 --write-baseline 另存一份基线。');
340
+ }
341
+
342
+ for (const key of VERDICT_KEYS) {
343
+ if (key === GATED_KEY) continue;
344
+ const delta = grew(key);
345
+ if (delta.length > 0) {
346
+ console.log(`⚠ 棘轮超基线(不红):${key} 比基线多 ${delta.length} 条 —— 新增项:`);
347
+ for (const id of delta) console.log(` ${id}`);
348
+ console.log(' (这一类**刻意不红**:它含大量"只导出给测试"的合法形态,判红等于焊死门禁。见文件头「门禁口径」)');
349
+ }
350
+ }
351
+
352
+ if (newDead.length > 0) {
353
+ console.log(`\n❌ 门禁红:新增 ${newDead.length} 个「谁都不用」的**真死符号**(生产零调用 ∧ 测试零引用 ∧ 本文件零自用)`);
354
+ for (const id of newDead) console.log(` ${id}`);
355
+ console.log(' 处置二选一(不许留着):接到生产调用点,或按 P5 纪律连同它的路径一起删除。');
356
+ } else if (counts[GATED_KEY] > gatedBaseline) {
357
+ // ★ 只在**计数真的超基线**时打这一句。`failed` 为真还有第三种来源(范围不一致),
358
+ // 那一句已在上面单独报过——同一件事报两遍、且第二遍的措辞与事实不符("0 > 0"),
359
+ // 正是本脚本反复在修的"输出与事实不一致"。
360
+ console.log(`\n❌ 门禁红:${GATED_KEY} ${counts[GATED_KEY]} 条 > 基线允许的 ${gatedBaseline} 条(名单与基线一致,是基线本身过宽)`);
361
+ }
362
+ if (vanishedDead.length > 0) {
363
+ console.log(`\n★ 棘轮可收紧:${GATED_KEY} 比基线少 ${vanishedDead.length} 条 —— 请重跑 --write-baseline 把它固化(棘轮只许往下走)`);
364
+ for (const id of vanishedDead) console.log(` ${id}`);
365
+ }
366
+ if (!failed && newDead.length === 0) {
367
+ console.log(`\n✅ 门禁通过:${GATED_KEY} ${counts[GATED_KEY]} 条,无新增真死符号。`);
368
+ }
369
+ process.exit(failed ? 1 : 0);
370
+ }
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // 只读本次明确选定的cache miss;复用原逐切片/逐实体SHA算法,禁止索引、模型或持久摘要。
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { createHash } = require('node:crypto');
6
+ const identity = file => { const stat = fs.statSync(file); return JSON.stringify([fs.realpathSync(file), stat.dev, stat.ino]); };
7
+ const scriptHash = createHash('sha256').update(fs.readFileSync(__filename)).digest('hex');
8
+ process.once('message', async job => {
9
+ let store;
10
+ let response;
11
+ try {
12
+ if (!job || typeof job.requestId !== 'string' || job.scriptHash !== scriptHash || !path.isAbsolute(job.databasePath)
13
+ || !Array.isArray(job.paths) || !job.paths.length || job.paths.some(file => typeof file !== 'string' || !file)
14
+ || JSON.stringify([...new Set(job.paths)].sort()) !== JSON.stringify(job.paths)
15
+ || identity(job.databasePath) !== job.databaseIdentity) throw new Error('只读解析身份请求或数据库身份无效');
16
+ const { IndexStateStore } = await import('@customize-agent/knowledge');
17
+ store = new IndexStateStore(job.databasePath, { readonly: true });
18
+ const results = store.readSnapshot(() => Object.fromEntries(job.paths.map(file => [file, store.parsedFileFingerprint(file)])));
19
+ if (identity(job.databasePath) !== job.databaseIdentity) throw new Error('只读计算期间数据库被替换');
20
+ response = { results };
21
+ } catch (error) { response = { error: error instanceof Error ? error.message : String(error) }; }
22
+ finally { store?.close(); }
23
+ if (!process.send) process.exit(1);
24
+ process.send({ requestId: job?.requestId, pid: process.pid, scriptHash, databaseIdentity: job?.databaseIdentity, paths: job?.paths, ...response }, error => {
25
+ process.exit(error || response.error ? 1 : 0);
26
+ });
27
+ });
28
+ process.on('disconnect', () => process.exit(1));
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  // 知识库后台索引子进程壳:索引主循环复用 @customize-agent/knowledge 的 runIndexLoop
3
- // (与主进程 runInProcess 共用同一实现),本文件只负责参数解析、进程壳与日志 IPC 转发。
3
+ // 本文件负责参数解析、独立进程与日志 IPC 转发。
4
4
  // 操作日志单写者为主进程:worker 不再直接读写 kb-operations.jsonl,
5
5
  // 而是通过 process.send({ type: 'log', patch }) 把补丁交给主进程统一落盘,
6
6
  // 避免父子进程同时读改写日志文件造成记录丢失。
7
7
  const path = require('node:path');
8
+ const { completeWorker } = require('./kb-worker-ipc.cjs');
8
9
 
9
10
  function upsert(_projectRoot, patch) {
10
11
  if (process.send) process.send({ type: 'log', patch });
@@ -12,7 +13,6 @@ function upsert(_projectRoot, patch) {
12
13
 
13
14
  function toStage(stage) {
14
15
  if (['parsing', 'chunking', 'vectorizing', 'done', 'error'].includes(stage)) return stage;
15
- if (stage === 'indexing') return 'chunking';
16
16
  if (stage === 'scanning') return 'uploading';
17
17
  return 'vectorizing';
18
18
  }
@@ -42,33 +42,40 @@ async function main() {
42
42
  });
43
43
  };
44
44
 
45
- const { vectorStatus } = await knowledge.runIndexLoop(project, job, onProgress);
45
+ const { vectorStatus, failedFiles } = await knowledge.runIndexLoop(project, job, onProgress);
46
+ if (failedFiles.length) {
47
+ const error = failedFiles.map(item => `${item.relativePath}:${item.reason}`).join('\n');
48
+ await completeWorker({ id: operationId, type: operationType, title: operationTitle, stage: 'error', status: 'error', percent: 100,
49
+ message: `索引结束,${failedFiles.length} 个文件解析失败;成功文件已保留`, error, filePath: job.relativePath, fileName: job.relativePath ? path.basename(job.relativePath) : undefined }, { shutdown: () => manager.shutdown() });
50
+ return;
51
+ }
46
52
  // 向量索引失败/降级不阻断上传:文本解析与切片已入库,操作以 warning 完成(主进程据 status='warning' 保留降级原因)
47
53
  if (vectorStatus.status === 'error' || vectorStatus.status === 'unavailable') {
48
54
  const error = vectorStatus.error || (vectorStatus.status === 'unavailable' ? 'hnswlib-node native 绑定不可用,向量索引已降级' : 'HNSWLib 向量入库失败');
49
- upsert(projectRoot, { id: operationId, type: operationType, title: operationTitle, stage: 'done', status: 'warning', percent: 100, message: job.relativePaths?.length ? '文件夹重新解析完成(向量索引降级)' : job.relativePath ? '单文件重新解析完成(向量索引降级)' : '知识库后台索引完成(向量索引降级)', filePath: job.relativePath, fileName: job.relativePath ? path.basename(job.relativePath) : undefined, error });
50
- try { await manager.shutdown(); } catch { /* 忽略关闭异常,不掩盖索引结果 */ }
51
- setImmediate(() => process.kill(process.pid, 'SIGKILL'));
55
+ await completeWorker({ id: operationId, type: operationType, title: operationTitle, stage: 'done', status: 'warning', percent: 100, message: job.relativePaths?.length ? '文件夹重新解析完成(向量索引降级)' : job.relativePath ? '单文件重新解析完成(向量索引降级)' : '知识库后台索引完成(向量索引降级)', filePath: job.relativePath, fileName: job.relativePath ? path.basename(job.relativePath) : undefined, error }, { shutdown: () => manager.shutdown() });
52
56
  return;
53
57
  }
54
- upsert(projectRoot, { id: operationId, type: operationType, title: operationTitle, stage: 'done', status: 'success', percent: 100, message: job.relativePaths?.length ? '文件夹重新解析完成' : job.relativePath ? '单文件重新解析完成' : '知识库后台索引完成', filePath: job.relativePath, fileName: job.relativePath ? path.basename(job.relativePath) : undefined });
55
- // 索引完成后显式释放 native 资源并自然退出。
56
- // 根因:直接 process.exit(0) 或未 shutdown 自然退出,都会在 native 模块
57
- // (hnswlib-node 向量索引)析构阶段触发 SIGABRT(libc++abi: mutex lock failed),
58
- // 主进程会误报“退出码 unknown”并把成功任务覆盖为失败。
59
- // shutdown 后进程已无任何待完成工作(数据已落盘、IPC 已同步写入管道),
60
- // 最后用 SIGKILL 自终结绕过 Node teardown(fork 场景实测 process.exit(0)、
61
- // disconnect+自然退出均会 SIGABRT);主进程据 receivedDone 防护把任务视为成功。
62
- try { await manager.shutdown(); } catch { /* 忽略关闭异常,不掩盖索引成功结果 */ }
63
- setImmediate(() => process.kill(process.pid, 'SIGKILL'));
58
+ // runIndexLoop 已完成持久化。等待终态 IPC 写入,再关闭资源和绕过 native teardown。
59
+ await completeWorker({ id: operationId, type: operationType, title: operationTitle, stage: 'done', status: 'success', percent: 100, message: job.relativePaths?.length ? '文件夹重新解析完成' : job.relativePath ? '单文件重新解析完成' : '知识库后台索引完成', filePath: job.relativePath, fileName: job.relativePath ? path.basename(job.relativePath) : undefined }, { shutdown: () => manager.shutdown() });
64
60
  }
65
61
 
66
- main().catch(error => {
62
+ main().catch(async error => {
67
63
  const job = JSON.parse(process.argv[2] || '{}');
68
64
  const projectRoot = job.projectRoot ? path.resolve(job.projectRoot) : process.cwd();
69
65
  const operationId = job.operationId || job.id || `worker-error-${Date.now()}`;
70
66
  const message = error instanceof Error ? (error.stack || error.message) : String(error);
71
- upsert(projectRoot, { id: operationId, type: job.uploadOperationId ? 'upload' : 'reindex', title: job.uploadOperationId ? `上传 ${job.uploadTitle || '文件'}` : job.relativePath ? `重新解析 ${job.relativePath}` : '知识库后台索引', stage: 'error', status: 'error', percent: 100, message, error: message, filePath: job.relativePath, fileName: job.relativePath ? path.basename(job.relativePath) : undefined });
72
- // 同样避免 teardown 阶段的 SIGABRT 掩盖真实错误信息(主进程据 error 补丁已落盘)
73
- setImmediate(() => process.kill(process.pid, 'SIGKILL'));
67
+ // 清理残留的进行中索引任务(解析抛错会停在 PARSING 等中间态),避免「假进行中」死任务让该文件无法删除
68
+ try {
69
+ const knowledge = await import('@customize-agent/knowledge');
70
+ const manager = new knowledge.MultiProjectManager();
71
+ const project = await manager.getProject(projectRoot);
72
+ project.failPendingIndexJobs(message);
73
+ await manager.shutdown();
74
+ } catch { /* 清理失败不掩盖原始错误 */ }
75
+ try {
76
+ await completeWorker({ id: operationId, type: job.uploadOperationId ? 'upload' : 'reindex', title: job.uploadOperationId ? `上传 ${job.uploadTitle || '文件'}` : job.relativePath ? `重新解析 ${job.relativePath}` : '知识库后台索引', stage: 'error', status: 'error', percent: 100, message, error: message, filePath: job.relativePath, fileName: job.relativePath ? path.basename(job.relativePath) : undefined }, { shutdown: async () => {} });
77
+ } catch (ipcError) {
78
+ console.error('[kb-worker] terminal IPC failed:', ipcError);
79
+ process.kill(process.pid, 'SIGKILL');
80
+ }
74
81
  });
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ /** 等 IPC 写入回调确认终态已送入管道后才释放资源/终结进程。 */
4
+ function sendTerminalPatch(patch, channel = process, timeoutMs = 10000) {
5
+ if (patch.percent !== 100 || !((patch.stage === 'done' && ['success', 'warning'].includes(patch.status)) || (patch.stage === 'error' && patch.status === 'error'))) {
6
+ return Promise.reject(new Error('知识库 worker 终态补丁无效'));
7
+ }
8
+ return new Promise((resolve, reject) => {
9
+ if (typeof channel.send !== 'function' || channel.connected === false) {
10
+ reject(new Error('知识库 worker IPC 通道不可用'));
11
+ return;
12
+ }
13
+ const timer = setTimeout(() => reject(new Error('知识库 worker 终态 IPC 写入超时')), timeoutMs);
14
+ try {
15
+ channel.send({ type: 'log', patch }, error => {
16
+ clearTimeout(timer);
17
+ if (error) reject(error);
18
+ else resolve();
19
+ });
20
+ } catch (error) {
21
+ clearTimeout(timer);
22
+ reject(error);
23
+ }
24
+ });
25
+ }
26
+
27
+ async function completeWorker(patch, { channel = process, shutdown, terminate = () => process.kill(process.pid, 'SIGKILL') }) {
28
+ await sendTerminalPatch(patch, channel);
29
+ // 数据写入在 runIndexLoop 内已 await 完成;这里释放 sqlite/native 句柄。
30
+ try { await shutdown(); } catch (error) { console.error('[kb-worker] shutdown:', error); }
31
+ terminate();
32
+ }
33
+
34
+ module.exports = { sendTerminalPatch, completeWorker };
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Next 产物清理:`dev` / `build` / `prepack` 三处共用的一条实现。
4
+ *
5
+ * 原先这三条 npm script 各自内联一份 `node -e "rmSync(...)"`(路径清单与重试魔数
6
+ * 复制三份,改一处得记得改三处),这里收成单一来源。
7
+ *
8
+ * ## 两种模式与理由
9
+ * - `full`:整体清空 `.next`,用在 `next dev` 启动前与 `next build` 前。
10
+ * 两种模式共用同一目录:dev 产物会污染生产构建(`verify-next-static` 把
11
+ * `static/development`、`static/webpack` 明确判为污染),生产产物也会让 dev 状态错乱。
12
+ * - `publish`:只清 `cache` 与 `static/{development,webpack}`,用在构建后与 `prepack`。
13
+ * `files` 把 `.next/static` 整目录打进发布包,dev 产物若不清会被 `verify-next-static`
14
+ * 当场拒绝;`cache` 是构建缓存,没有发布价值。
15
+ *
16
+ * ## 为什么带重试
17
+ * `rmSync` 遇 EBUSY/ENOTEMPTY 默认直接抛——之前出现过删除中途被占用(旧 next 进程 /
18
+ * 编辑器 / 杀软持有句柄)导致构建链中断。`maxRetries + retryDelay` 让它自己退避重试。
19
+ *
20
+ * 用法:
21
+ * node scripts/purge-next-artifacts.cjs full # 整个 .next
22
+ * node scripts/purge-next-artifacts.cjs publish # cache + static/{development,webpack}
23
+ * 退出码:0=已清(含本就干净的);1=模式缺失/未知。
24
+ */
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+
28
+ const nextDir = path.resolve(__dirname, '..', '.next');
29
+
30
+ /** 占用重试:与两条 npm script 原先的内联参数保持一致 */
31
+ const REMOVE_OPTIONS = { recursive: true, force: true, maxRetries: 5, retryDelay: 200 };
32
+
33
+ const MODES = {
34
+ full: () => {
35
+ fs.rmSync(nextDir, REMOVE_OPTIONS);
36
+ },
37
+ publish: () => {
38
+ for (const target of [
39
+ path.join(nextDir, 'cache'),
40
+ path.join(nextDir, 'static', 'development'),
41
+ path.join(nextDir, 'static', 'webpack'),
42
+ ]) {
43
+ fs.rmSync(target, REMOVE_OPTIONS);
44
+ }
45
+ },
46
+ };
47
+
48
+ function usage() {
49
+ console.log(`Usage: node scripts/purge-next-artifacts.cjs <full|publish>
50
+
51
+ full Remove the whole .next directory (before \`next dev\` / \`next build\`).
52
+ publish Remove .next/cache and .next/static/{development,webpack} (after build / before pack).`);
53
+ }
54
+
55
+ function main() {
56
+ const args = process.argv.slice(2);
57
+ if (args.includes('--help') || args.includes('-h')) {
58
+ usage();
59
+ return 0;
60
+ }
61
+ const run = args.length === 1 ? MODES[args[0]] : undefined;
62
+ if (!run) {
63
+ usage();
64
+ return 1;
65
+ }
66
+ run();
67
+ return 0;
68
+ }
69
+
70
+ if (require.main === module) process.exit(main());
@@ -15,7 +15,7 @@ const required = [
15
15
  for (const item of required) {
16
16
  if (!fs.existsSync(item)) {
17
17
  console.error(`[server] Missing Next build artifact: ${path.relative(root, item)}`);
18
- console.error('[server] Run `pnpm build` in apps/server before `pnpm start`.');
18
+ console.error('[server] Run `pnpm build` at the monorepo root (or in apps/server) before `pnpm start`.');
19
19
  process.exit(1);
20
20
  }
21
21
  }
@@ -54,7 +54,7 @@ for (const file of files) {
54
54
  const pagesManifestPath = path.join(nextDir, 'server', 'pages-manifest.json');
55
55
  if (!fs.existsSync(pagesManifestPath)) {
56
56
  console.error('[server] Missing Next pages manifest: .next/server/pages-manifest.json');
57
- console.error('[server] Run `pnpm build` in apps/server before `pnpm start`.');
57
+ console.error('[server] Run `pnpm build` at the monorepo root (or in apps/server) before `pnpm start`.');
58
58
  process.exit(1);
59
59
  }
60
60
  const pagesManifest = JSON.parse(fs.readFileSync(pagesManifestPath, 'utf8'));