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",
"--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) => {
}
});
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);
});
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" });
<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>