]> acesimba.cloud Git - StationBuilder.git/commitdiff
AI 各项功能并发调用数统一限制为 2
authorroot <root@localhost.localdomain>
Thu, 20 Aug 2026 12:17:34 +0000 (20:17 +0800)
committerroot <root@localhost.localdomain>
Thu, 20 Aug 2026 12:17:34 +0000 (20:17 +0800)
- db-sidecar.mjs:新增全局信号量 AI_CONCURRENCY=2 包裹 runCodebuddy,
  使站点生成(含批内并行)、各项补全、领域方案补全全部 AI 功能任意时刻最多
  2 个 codebuddy 子进程并发;release 在 close/error/超时 三处统一释放并加 released
  守卫与同步 spawn 失败保护,避免死锁。
- 站点「生成全部」批内并行数 GEN_CONCURRENCY 由硬编码 3 改为复用 AI_CONCURRENCY,
  对应日志文案同步为「最多 2 个并行调用」。
- ai-sidecar.mjs(/api/chat) 本身以 running 布尔量串行(≤1),已满足 ≤2。
- design-doc.html 升 v0.24 并登记变更。
- 注:未提交 data/db/StationBuilder.sqlite(无 schema 变更,且工作库含累积业务数据)。

db-sidecar.mjs
public/design-doc.html

index 0f15f23f24206f1046acd2a442d9f5f2e2acc40c..de47d9658adebfecc310e7608495685f45a95e9b 100644 (file)
@@ -628,7 +628,25 @@ function repairAndValidate(json) {
   return { ok: errors.length === 0, json: fixed, errors };
 }
 
-function runCodebuddy(prompt, sessionId, onEvent) {
+// 全局 AI 并发上限:任意时刻最多 AI_CONCURRENCY 个 codebuddy 子进程并发,
+// 覆盖站点生成(含批内并行)、各项补全、领域方案补全——即 StationBuilder 全部 AI 功能。
+const AI_CONCURRENCY = 2;
+const aiSem = { active: 0, waiters: [] };
+function aiAcquire() {
+  return new Promise((resolve) => {
+    if (aiSem.active < AI_CONCURRENCY) { aiSem.active++; resolve(); }
+    else aiSem.waiters.push(resolve);
+  });
+}
+function aiRelease() {
+  if (aiSem.active > 0) aiSem.active--;
+  if (aiSem.waiters.length) { aiSem.active++; aiSem.waiters.shift()(); }
+}
+
+async function runCodebuddy(prompt, sessionId, onEvent) {
+  await aiAcquire();
+  let released = false;
+  const release = () => { if (!released) { released = true; aiRelease(); } };
   return new Promise((resolve, reject) => {
     const args = [
       "--model", "hy3",
@@ -640,11 +658,18 @@ function runCodebuddy(prompt, sessionId, onEvent) {
       "--tools", "",
       "-y",
     ];
-    const child = spawn(CODEBUDDY, args, { env: process.env, cwd: CWD });
+    let child;
+    try {
+      child = spawn(CODEBUDDY, args, { env: process.env, cwd: CWD });
+    } catch (e) {
+      release();
+      return reject(e);
+    }
     let buf = "";
     let fullText = "";
     const timer = setTimeout(() => {
       child.kill("SIGKILL");
+      release();
       reject(new Error("codebuddy 生成超时(>480s)"));
     }, 480000);
     const push = (line) => {
@@ -666,10 +691,11 @@ function runCodebuddy(prompt, sessionId, onEvent) {
       }
     });
     child.stderr.on("data", c => push(c.toString("utf-8")));
-    child.on("error", reject);
+    child.on("error", (e) => { release(); reject(e); });
     child.on("close", code => {
       clearTimeout(timer);
       if (buf.trim()) push(buf);
+      release();
       if (code && code !== 0) return reject(new Error("codebuddy exit " + code));
       resolve(fullText);
     });
@@ -1516,10 +1542,10 @@ async function handleGenerateAll(req, res, siteId) {
         streamEvent(res, { log: o.event.delta.text });
       }
     };
-    streamEvent(res, { log: `\n▶ 开始站点生成:最多 3 个并行调用 Wordbuddy AI(Hy3)与 dashboard-json-generator Skill 生成 ${cubes.length} 个 Cube 页面…\n` });
+    streamEvent(res, { log: `\n▶ 开始站点生成:最多 ${AI_CONCURRENCY} 个并行调用 Wordbuddy AI(Hy3)与 dashboard-json-generator Skill 生成 ${cubes.length} 个 Cube 页面…\n` });
     streamEvent(res, { task: "模型准备", status: "done" });
-    // ⑥ 并发生成:各 Cube 互为独立,但限制最多 3 个同时进行,避免资源争抢导致超时。
-    const GEN_CONCURRENCY = 3;
+    // ⑥ 并发生成:各 Cube 互为独立,但限制最多 AI_CONCURRENCY 个同时进行,避免资源争抢导致超时(全局 AI 并发上限见 runCodebuddy)。
+    const GEN_CONCURRENCY = AI_CONCURRENCY;
     const onLog = (t) => streamEvent(res, { log: t });
     await mapWithConcurrency(cubes, GEN_CONCURRENCY, async (cube) => {
       streamEvent(res, { task: cube.name, status: "running" });
index 051c4d35982fce0f21ecefa4e483c1dfb0b3d7e8..319aff9260fe945dc2bc712d3e7278c81b26e777 100644 (file)
   <nav class="doc-nav"><div class="doc-nav-inner" id="toc"></div></nav>
 
 <main id="doc">
-  <p class="muted">文档版本:v0.23 最后更新:2026-08-20 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
+  <p class="muted">文档版本:v0.24 最后更新:2026-08-20 修改人:wang.yu 状态:开发中(演示数据已标注)</p>
   <h1>StationBuilder 产品与交互设计方案</h1>
 
   <h2>1. 文档说明</h2>
   <h3>11.1 版本变更记录(Changelog)</h3>
   <table class="changelog">
     <tr><th>版本</th><th>日期</th><th>修改人</th><th>说明</th></tr>
+    <tr><td>v0.24</td><td>2026-08-20</td><td>wang.yu</td><td>AI 并发调用统一限流:新增全局信号量 <code>AI_CONCURRENCY = 2</code>,包裹 <code>runCodebuddy</code>(db-sidecar.mjs),使站点生成(含批内并行)、各项补全(complete / complete-assets / complete-cubes)、领域方案补全(dt-complete 系列)等全部 AI 功能任意时刻最多 2 个 codebuddy 子进程并发,避免无脑全并行抢占资源导致超时。站点「生成全部」批内并行数 <code>GEN_CONCURRENCY</code> 由硬编码 3 改为复用 <code>AI_CONCURRENCY</code>,对应日志文案同步为「最多 2 个并行调用」。AI 聊天侧(ai-sidecar.mjs /api/chat)本身以 <code>running</code> 布尔量串行(同一时刻仅 1 个),已满足 ≤2。</td></tr>
     <tr><td>v0.23</td><td>2026-08-20</td><td>wang.yu</td><td>领域知识上传格式扩展:<code>KnowledgeStep</code> 上传 <code>accept</code> 由纯文本类放开至 Word / Excel / PDF / 图片及常见文本(<code>.pdf,.doc,.docx,.xls,.xlsx,image/*</code> 等);提示文案补充「Word、Excel、PDF 会提取正文注入,图片仅作参考登记」,列表文件类徽标「文档」改为「文件」以兼容图片。后端 <code>extractDocText(filePath,mime,filename)</code> 替换原 <code>isTextMime</code>/<code>readKnowledgeFile</code>:文本类(txt/md/json/csv/html/xml/yaml/yml 等)直接读取;PDF(<code>pdf-parse</code>)、Word(仅 <code>.docx</code>,<code>mammoth</code>)、Excel(<code>.xls</code>/<code>.xlsx</code>,SheetJS 转 CSV)提取正文并 16k 截断后注入提示词;图片及老 <code>.doc</code> 等无法纯 JS 提取的格式返回空串,<code>buildKnowledgePrompt</code> 中仍登记为参考来源(<code>(参考来源:文件名)</code>)。新增依赖 <code>pdf-parse</code> / <code>mammoth</code> / <code>xlsx</code>(纯 JS,无原生编译)。</td></tr>
     <tr><td>v0.22</td><td>2026-08-20</td><td>wang.yu</td><td>领域知识提升为方案创建流程 Step 2,并作为 AI 补全的优先参考资料:① 方案向导步骤重排——领域知识由末段移至「方案基本属性」之后(Step 2),原 资产设置 / 功能与菜单 / 运维流程 / 生成保存 / 专家确认 整体后移一位(<code>DOMAIN_STEPS</code> 顺序调整,key 不变,步进导航自动适配)。② 新增「领域知识」步骤(<code>KnowledgeStep</code>):支持「上传领域知识文档」(多文件,base64 经既有 <code>POST /api/domain-templates/:id/docs</code> 落库,文本类读正文、其他格式登记为参考)与「配置参考 URL 站点」(<code>doc_type=link</code>),列表展示名称/类型/大小/时间并提供「下载」(仅文档)与「删除」(<code>DELETE /api/knowledge-docs/:id</code>,同步清理磁盘文件);知识随方案持久化、可删。③ 知识驱动 AI 补全:新增 <code>buildKnowledgePrompt(ctx)</code>(镜像 <code>buildStylePrompt</code>),读取本方案 <code>knowledge_docs</code>——文本文档直接注入正文、URL 站点由服务端 <code>fetch</code> 抓取正文注入(8s 超时,失败降级为仅保留 URL 链接作为参考),注入到 <code>buildCompletePrompt</code> / <code>buildCompleteAssetsPrompt</code> / <code>buildCompleteCubesPrompt</code> 三处(三个构建器改为 async、8 处调用点加 await;领域级补全 ctx 增加 <code>id</code>,站点级无 id 不注入)。优先级为「优先依据已上传领域知识分析补全,再做大模型在线补充」。④ 生成保存(<code>DomainGenerateReview</code>)「领域知识」行由「(待开发)」改为真实文档信息(份数 + 名称列举),并提供「返回领域知识」快捷入口;运维流程 AI 补全本期仍占位(<span class="todo">尚未实现</span>)。</td></tr>
     <tr><td>v0.21</td><td>2026-08-20</td><td>wang.yu</td><td>领域方案库 Cube 扩展页面支持选择页面样式库样式后生成(镜像站点侧 <code>sites.style_id</code> 机制,方案级):① 后端 <code>domain_templates</code> 新增 <code>style_id</code> 列(<code>ensureColumn</code> 增量加列);<code>handleDomainTemplateUpdate</code> 支持写入/显式置空;<code>hydrateDomainTemplateCtx</code> 返回真实 <code>style_id</code>(此前硬编码 null)。② 前端领域方案详情「功能与菜单 → Cube 扩展页面」顶部新增「领域方案页面样式」选择器(复用 <code>.style-picker</code> 与页面样式库缩略图),选中即 PUT <code>/domain-templates/:id</code> 持久化;生成/AI 修改经共享的 <code>buildStylePrompt</code> 自动注入所选样式的 JSON 主题,无需改动提示词构造。</td></tr>