import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
import { spawn } from "node:child_process";
import { createReadStream } from "node:fs";
import { mkdir, readFile, realpath, rm, stat, statfs, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import cookieParser from "cookie-parser";
import express from "express";
import { ipKeyGenerator, rateLimit } from "express-rate-limit";
import helmet from "helmet";
import multer from "multer";
import { chooseHermesIntent, classifyWorkspaceRisk, generateAnswer, getTextModelStatus, moduleFor, redactSensitive } from "./lib/ai.mjs";
import { createAgentHarnessClient, harnessCapabilityFor, publicHarnessState, sha256Summary } from "./lib/agent-harness.mjs";
import { AuthError, createAuth, parseBearerToken } from "./lib/auth.mjs";
import { createChatProgressHub } from "./lib/chat-progress.mjs";
import { findDuplicates, fingerprintTopic } from "./lib/content.mjs";
import { buildCoursewareGuizangHtml, buildCoursewarePptx, buildSpeakerNotes, getCoursewareRuntimeStatus } from "./lib/courseware.mjs";
import { CREDIT_POLICY, getRechargeCatalog } from "./lib/credits.mjs";
import { buildLessonPlanDocx, getDocumentRuntimeStatus } from "./lib/document.mjs";
import { createCrm } from "./lib/crm.mjs";
import { createCrmReadonlyClient, CrmReadonlyError } from "./lib/crm-readonly.mjs";
import { createCrmOutreachClient } from "./lib/crm-outreach.mjs";
import { extractAndChunkDocument, UnsupportedDocumentError } from "./lib/extract.mjs";
import { createExternalKnowledgeClient } from "./lib/external-knowledge.mjs";
import { createGroupAgentTriggerGuard, parseGroupAgentMentions, publicGroupAgents, removeGroupAgentMentions } from "./lib/group-agent-mentions.mjs";
import { createGeoService } from "./lib/geo/geo-service.mjs";
import { createGeoStore } from "./lib/geo/geo-store.mjs";
import { createGeoConnectorRegistry } from "./lib/geo/geo-connectors.mjs";
import { createGeoBudgetGuard } from "./lib/geo/geo-budget.mjs";
import { createLangGraphAdapter } from "./lib/langgraph-adapter.mjs";
import { brandGeneratedImage } from "./lib/image-branding.mjs";
import { createLibImageClient, IMAGE_MODES } from "./lib/libimage.mjs";
import { renderKnowledgeCardPng, renderKnowledgeCardSvg } from "./lib/knowledge-card-renderer.mjs";
import { createRechargeGateway } from "./lib/recharge.mjs";
import { canViewDocument, isCustomerPathExcluded, searchKnowledge } from "./lib/search.mjs";
import { buildSpeechGlossary, correctSpeechTranscript, SPEECH_CORRECTION_MAX_LENGTH } from "./lib/speech-correction.mjs";
import { AUTO_CUT_TEMPLATE_KEY, autoCutTemplateList, buildAutoCutPrompt, getAutoCutTemplate, normalizeAutoCutSource, parseAutoCutPlan } from "./lib/auto-cut.mjs";
import { autoCutOssConfigured, createAutoCutOssConfig, getAutoCutOssSignedFileUrl, listAutoCutOssDirectory, normalizeAutoCutOssPath } from "./lib/auto-cut-oss.mjs";
import { createAutoCutRunStore } from "./lib/auto-cut-runs.mjs";
import { buildWorkspaceObjectKey, createWorkspaceObjectStore } from "./lib/workspace-object-store.mjs";
import { advanceAutoCutStage, publicAutoCutRun } from "./lib/auto-cut-workflow.mjs";
import { executeAutoCutWorkflow } from "./lib/auto-cut-worker.mjs";
import { createWechatRunStore } from "./lib/wechat-runs.mjs";
import { publicWechatRun, WECHAT_STAGES } from "./lib/wechat-workflow.mjs";
import { executeWechatWorkflow } from "./lib/wechat-worker.mjs";
import { createWechatHermesPlanner } from "./lib/wechat-hermes.mjs";
import { buildWechatPackage } from "./lib/wechat-packager.mjs";
import { createAutoCutMiniMaxPlanner } from "./lib/auto-cut-minimax.mjs";
import { createStore } from "./lib/store.mjs";
import {
  buildHermesTaskPrompt,
  publicWorkspaceTask,
  publicWorkspaceTaskDetail,
  resolveWorkspaceTaskPolicy,
} from "./lib/task-contract.mjs";
import { createLovartClient, IMAGE_MODELS, validateGeneratedImage } from "./lib/lovart.mjs";
import {
  createPublicSourceKnowhowSearch,
  DEFAULT_KNOWHOW_MODEL_ID,
  isInvalidCollectorItem,
  KNOWHOW_STREAMS,
  parseKnowhowCandidates,
} from "./lib/knowhow-auto-collector.mjs";
import { CRM_TEXT_MODEL_ID, DEFAULT_TEXT_MODEL_ID, normalizeTextModelId, TEXT_MODEL_IDS, TEXT_MODEL_SWITCH_ENABLED } from "./lib/text-models.mjs";
import { createWeComClient, WeComError } from "./lib/wecom.mjs";
import { createWeComCallbackCrypto } from "./lib/wecom-callback.mjs";
import { createWeComContentTaskStore } from "./lib/wecom-content-tasks.mjs";
import { listReleaseDirectory, releaseCenterHealth, resolveReleasePath } from "./lib/release-center-local.mjs";
import { getManifestFile, listManifestDirectory, releaseCenterManifestHealth } from "./lib/release-center-manifest.mjs";
import { expandKnowledgeCardSelectionPrompt, knowledgeCardMetaForPrompt } from "../shared/knowledge-card.mjs";

const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = resolve(__dirname, "..");
const RELEASE_CENTER_LOCAL_ROOT = resolve(
  process.env.RELEASE_CENTER_LOCAL_ROOT || "/mnt/chuang-workspace-offsite/release-center/O002_创创公司",
);
const RELEASE_CENTER_MANIFEST_PATH = resolve(
  process.env.RELEASE_CENTER_MANIFEST_PATH || join(projectRoot, "release-center-manifest.json"),
);
const COOKIE_NAME = "cc_workspace_session";
const WECOM_STATE_COOKIE = "cc_workspace_wecom_state";
const KNOWLEDGE_SCOPES = new Set(["智能匹配", "销售与 CRM", "课程课件", "内容与选题", "公司通用知识", "我的资料"]);
const CRM_EXPERT_SCOPE = "销售与 CRM";
const KNOWLEDGE_ENABLED_SCOPES = new Set(["智能匹配", "课程课件", "内容与选题", "公司通用知识", "我的资料"]);
const FILE_SCOPES = new Set(["personal", "department", "company", "boss"]);
const GROUP_IMAGE_MIME_BY_EXTENSION = new Map([
  [".png", "image/png"],
  [".jpg", "image/jpeg"],
  [".jpeg", "image/jpeg"],
  [".webp", "image/webp"],
  [".gif", "image/gif"],
]);
const GENERATED_IMAGE_MIME_BY_EXTENSION = new Map([
  ...GROUP_IMAGE_MIME_BY_EXTENSION,
  [".svg", "image/svg+xml"],
]);
const LOVART_IMAGE_MIME_BY_EXTENSION = new Map([
  [".png", "image/png"],
  [".jpg", "image/jpeg"],
  [".jpeg", "image/jpeg"],
  [".webp", "image/webp"],
]);
const ALLOWED_EXTENSIONS = new Set([
  ".txt", ".md", ".markdown", ".csv", ".json", ".pdf", ".docx", ".pptx", ".xlsx",
  ...GROUP_IMAGE_MIME_BY_EXTENSION.keys(),
]);
const AUTO_CUT_VIDEO_EXTENSIONS = new Set([".mp4", ".mov", ".m4v", ".webm"]);
const AUTO_CUT_VIDEO_MIME_TYPES = new Set(["video/mp4", "video/quicktime", "video/webm"]);
const AUTO_CUT_OSS_CONFIG = createAutoCutOssConfig();
const CONTENT_TOPIC_VISIBILITY = "company";
const KNOWLEDGE_TYPES = new Set(["制度", "产品", "客户案例", "培训资料", "项目经验", "其他"]);
const SENSITIVE_CATEGORIES = new Set(["客户隐私", "合同", "财务", "人事", "其他敏感信息"]);

function compactScopeText(value) {
  return String(value || "")
    .normalize("NFKC")
    .toLowerCase()
    .replace(/[\s\p{P}\p{S}_]+/gu, "");
}

async function sha256File(filePath) {
  const digest = createHash("sha256");
  for await (const chunk of createReadStream(filePath)) digest.update(chunk);
  return digest.digest("hex");
}

const RESTRICTED_CLIENT_NAMES = Object.freeze([
  "兴宇",
  ...String(process.env.RESTRICTED_CLIENT_NAMES || "").split(","),
].map(compactScopeText).filter(Boolean));

function containsRestrictedBusinessContent(value) {
  const compact = compactScopeText(value);
  if (!compact) return false;
  return /代运营客户|客户交付|客户项目|客户发布包|客户资料包|发布包/iu.test(compact)
    || RESTRICTED_CLIENT_NAMES.some((name) => compact.includes(name));
}

function isExplicitKnowledgeQuestion(prompt) {
  const value = String(prompt || "").replace(
    /(?:不要|无需|无须|不必|禁止|别|勿).{0,24}(?:公司)?知识库/giu,
    "",
  );
  return /《[^》]+》|(?:根据|依据).{0,16}(?:公司)?知识库|(?:引用|标注).{0,12}(?:资料|来源)|公司知识库/iu.test(value);
}

function resolvePrivateChatRoute(prompt, knowledgeScope) {
  const selectedBusinessModule = ["销售与 CRM", "课程课件", "内容与选题"].includes(knowledgeScope)
    ? knowledgeScope
    : undefined;
  const explicitKnowledgeQuestion = isExplicitKnowledgeQuestion(prompt);
  const autoSelectedBusinessModule = knowledgeScope === "智能匹配" && !explicitKnowledgeQuestion
    ? moduleFor(prompt)
    : undefined;
  const effectiveModule = selectedBusinessModule || autoSelectedBusinessModule || "通用办公";
  const effectiveIntent = chooseHermesIntent({ prompt, module: effectiveModule });
  let routedScope = knowledgeScope;
  if (knowledgeScope === "智能匹配") {
    if (["课程课件", "内容与选题"].includes(autoSelectedBusinessModule)) routedScope = autoSelectedBusinessModule;
    if (autoSelectedBusinessModule === CRM_EXPERT_SCOPE && effectiveIntent === "crm_query") routedScope = CRM_EXPERT_SCOPE;
  }
  return {
    selectedBusinessModule,
    autoSelectedBusinessModule,
    effectiveModule,
    effectiveIntent,
    routedScope,
    autoRouted: Boolean(autoSelectedBusinessModule && autoSelectedBusinessModule !== "通用办公"),
  };
}

function buildProcessingTrace({
  effectiveModule,
  autoRouted,
  citations,
  attachments,
  crmReadonlyQueried,
  shouldSearchKnowledge,
  durationMs,
}) {
  const capabilityDetail = autoRouted
    ? `通用助手已自动匹配${effectiveModule}能力`
    : `使用${effectiveModule}能力处理`;
  let sourceDetail = "本次任务无需调用额外资料";
  if (crmReadonlyQueried) sourceDetail = "已读取当前账号权限内的 CRM 在线只读数据";
  else if (citations.length) sourceDetail = `已采用 ${citations.length} 份公司资料`;
  else if (attachments.length) sourceDetail = `已读取 ${attachments.length} 个本次对话附件`;
  else if (shouldSearchKnowledge) sourceDetail = "已检索授权资料，本次回答未采用资料";

  return {
    status: "completed",
    durationMs,
    summary: "已完成任务理解、能力匹配、资料使用和结果检查",
    steps: [
      { id: "understand", label: "理解任务", detail: `识别为${effectiveModule}任务并确认交付目标`, status: "completed" },
      { id: "route", label: "匹配能力", detail: capabilityDetail, status: "completed" },
      { id: "sources", label: crmReadonlyQueried ? "读取业务数据" : "查找资料", detail: sourceDetail, status: "completed" },
      { id: "check", label: "检查结果", detail: "已应用格式、引用和权限规则", status: "completed" },
    ],
  };
}

function isAttachmentOnlyQuestion(prompt, attachments) {
  if (!attachments?.length) return false;
  const value = String(prompt || "");
  if (/(?:结合|对照|同时使用).{0,12}(?:公司|知识库)|(?:公司|知识库).{0,12}(?:结合|对照)/iu.test(value)) return false;
  return /附件|文件|图片|这份|这张|总结|整理|提炼|阅读|识别|改写|润色|执行清单/iu.test(value);
}

function alignAnswerCitations(answer, citations) {
  if (!citations.length) return { answer, citations: [] };
  const used = [...String(answer || "").matchAll(/\[资料(\d+)\]/gu)]
    .map((match) => Number(match[1]))
    .filter((index) => Number.isInteger(index) && index > 0 && index <= citations.length);
  const ordered = [...new Set(used)];
  if (!ordered.length) return { answer, citations: [] };
  const remap = new Map(ordered.map((oldIndex, index) => [oldIndex, index + 1]));
  return {
    answer: String(answer).replace(/\[资料(\d+)\]/gu, (match, rawIndex) => remap.has(Number(rawIndex)) ? `[资料${remap.get(Number(rawIndex))}]` : ""),
    citations: ordered.map((oldIndex, index) => ({ ...citations[oldIndex - 1], index: index + 1 })),
  };
}

function normalizeUploadFilename(value) {
  const filename = String(value || "");
  const characters = [...filename];
  if (!characters.some((character) => character.charCodeAt(0) > 127)
    || characters.some((character) => character.charCodeAt(0) > 255)) return filename;
  const decoded = Buffer.from(filename, "latin1").toString("utf8");
  return decoded.includes("\uFFFD") ? filename : decoded.normalize("NFC");
}

function numberFromEnv(name, fallback) {
  const value = Number(process.env[name]);
  return Number.isFinite(value) && value > 0 ? value : fallback;
}

function safeDiagnosticCode(value, fallback = "UNKNOWN") {
  const normalized = String(value || "").toUpperCase();
  return /^[A-Z][A-Z0-9_]{0,63}$/u.test(normalized) ? normalized : fallback;
}

function safeRequestId(value) {
  const normalized = String(value || "");
  return /^[A-Za-z0-9][A-Za-z0-9_-]{7,63}$/u.test(normalized) ? normalized : randomUUID();
}

function diagnosticRoute(req) {
  if (typeof req.route?.path === "string") return `${req.baseUrl || ""}${req.route.path}`;
  if (req.path === "/workspace-api/healthz") return "/workspace-api/healthz";
  if (req.path.startsWith("/workspace-api/")) return "/workspace-api/unmatched";
  return "/workspace";
}

function secureEqualText(left, right) {
  const leftBuffer = Buffer.from(String(left || ""), "utf8");
  const rightBuffer = Buffer.from(String(right || ""), "utf8");
  return leftBuffer.length > 0 && leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
}

function weComRedirectErrorCode(error) {
  if (error?.code === "WECOM_MEMBER_INACTIVE") return "member_inactive";
  if (["WECOM_MEMBER_REQUIRED", "WECOM_MEMBER_NOT_VISIBLE", "WECOM_USER_NOT_PROVISIONED"].includes(error?.code)) return "member_not_allowed";
  if (error?.code === "WECOM_ACCOUNT_CONFLICT") return "account_conflict";
  if (["WECOM_CODE_INVALID", "WECOM_TOKEN_EXPIRED"].includes(error?.code)) return "expired";
  return "unavailable";
}

function serializeUser(user) {
  if (!user) return null;
  return {
    id: user.id,
    email: user.email,
    name: user.displayName || user.name || user.username || user.email,
    displayName: user.displayName || user.name || user.email,
    role: user.role,
    permissions: Array.isArray(user.permissions) ? user.permissions : [],
    departmentId: user.departmentId || null,
    enabled: user.status === "active",
    status: user.status,
    wecomBound: Boolean(user.wecomUserId || user.wecomBound),
    avatarUrl: user.avatarUrl || null,
    creditBalance: Number.isInteger(user.creditBalance) ? user.creditBalance : null,
    createdAt: user.createdAt,
    updatedAt: user.updatedAt,
  };
}

function serializeFile(file) {
  if (!file) return null;
  return {
    id: file.id,
    originalName: file.originalName,
    mimeType: file.mimeType,
    size: file.size,
    scope: file.scope,
    space: file.space || null,
    status: file.status,
    malwareScanStatus: file.malwareScanStatus || null,
    knowledgeDocumentId: file.knowledgeDocumentId || null,
    submitAsCompanyKnowledge: file.submitAsCompanyKnowledge === true,
    knowledgeType: file.knowledgeType || null,
    sensitive: file.sensitive === true,
    sensitiveCategories: Array.isArray(file.sensitiveCategories) ? file.sensitiveCategories : [],
    workflowStatus: file.workflowStatus || null,
    createdAt: file.createdAt,
    updatedAt: file.updatedAt,
  };
}

function newPaymentOrderNo() {
  return `CC${Date.now().toString(36)}${randomBytes(6).toString("hex")}`.slice(0, 32);
}

function serializePaymentOrder(order) {
  if (!order) return null;
  return {
    id: order.id,
    packageId: order.packageId,
    packageName: order.packageName,
    credits: order.credits,
    amountCents: order.amountCents,
    currency: order.currency || "CNY",
    channel: order.channel,
    outTradeNo: order.outTradeNo,
    status: order.status,
    createdAt: order.createdAt,
    paidAt: order.paidAt || null,
  };
}

function serializeGroupMember(user) {
  if (!user) return null;
  return {
    id: user.id,
    name: user.displayName || user.name || user.email,
    email: user.email || null,
    departmentId: user.departmentId || null,
    departmentNames: Array.isArray(user.wecomDepartmentNames) ? user.wecomDepartmentNames : [],
    wecomBound: Boolean(user.wecomUserId || user.wecomBound),
    avatarUrl: user.avatarUrl || null,
  };
}

function serializeGroupAttachment(file) {
  if (!file) return null;
  const extension = extname(file.originalName || "").toLowerCase();
  return {
    id: file.id,
    originalName: file.originalName,
    mimeType: file.mimeType,
    size: file.size,
    isImage: GROUP_IMAGE_MIME_BY_EXTENSION.has(extension),
    previewUrl: GROUP_IMAGE_MIME_BY_EXTENSION.has(extension) ? `/workspace-api/files/${encodeURIComponent(file.id)}/preview` : null,
    createdAt: file.createdAt,
  };
}

function serializeChatAttachment(file) {
  if (!file) return null;
  const extension = extname(file.originalName || "").toLowerCase();
  const isImage = GROUP_IMAGE_MIME_BY_EXTENSION.has(extension);
  return {
    id: file.id,
    originalName: file.originalName,
    mimeType: file.mimeType,
    size: file.size,
    isImage,
    previewUrl: isImage ? `/workspace-api/files/${encodeURIComponent(file.id)}/preview` : null,
    readableByAgent: !isImage && Boolean(file.extractedText),
    createdAt: file.createdAt,
  };
}

function serializeGeneratedImage(file) {
  if (!file) return null;
  return {
    id: file.id,
    originalName: file.originalName,
    mimeType: file.mimeType,
    size: file.size,
    model: file.imageModel,
    modelLabel: file.imageModelLabel,
    provider: file.imageProvider || null,
    previewUrl: `/workspace-api/files/${encodeURIComponent(file.id)}/preview`,
    downloadUrl: `/workspace-api/files/${encodeURIComponent(file.id)}/download`,
  };
}

function serializeImageJob(job, files = [], balance = null) {
  return {
    id: job.id,
    status: job.status,
    model: job.model,
    modelLabel: job.modelLabel,
    mode: job.mode || "text-to-image",
    provider: job.provider || null,
    creditCost: job.creditCost,
    creditRefunded: job.creditRefunded === true,
    conversationId: job.conversationId,
    messageId: job.messageId || null,
    artifacts: files.map(serializeGeneratedImage).filter(Boolean),
    errorCode: job.errorCode || null,
    errorMessage: job.errorMessage || null,
    createdAt: job.createdAt,
    startedAt: job.startedAt || null,
    completedAt: job.completedAt || null,
    balance,
  };
}

function serializeOutreachReview(review) {
  if (!review) return null;
  return {
    id: review.id,
    sourceKey: review.sourceKey,
    source: review.source || "CRM自动化",
    campaignName: review.campaignName || "客户经营触达",
    crmCustomerId: review.crmCustomerId,
    customerName: review.customerName || review.customerNameMasked || "待确认客户",
    customerNameMasked: review.customerNameMasked || null,
    contactMasked: review.contactMasked || null,
    ownerName: review.ownerName || "待确认负责人",
    reason: review.reason || "CRM自动分析建议触达",
    evidence: Array.isArray(review.evidence) ? review.evidence : [],
    riskLevel: review.riskLevel || "low",
    riskChecks: Array.isArray(review.riskChecks) ? review.riskChecks : [],
    content: review.content || "",
    tags: Array.isArray(review.tags) ? review.tags : [],
    status: review.status,
    approvalStatus: review.approvalStatus,
    deliveryStatus: review.deliveryStatus,
    approvalNote: review.approvalNote || null,
    rejectionReason: review.rejectionReason || null,
    approvedAt: review.approvedAt || null,
    rejectedAt: review.rejectedAt || null,
    submittedAt: review.submittedAt || null,
    deliveredAt: review.deliveredAt || null,
    wecomTaskId: review.wecomTaskId || null,
    crmOutreachTaskId: review.crmOutreachTaskId || null,
    lastErrorMessage: review.lastErrorMessage || null,
    submissionAttempts: Number(review.submissionAttempts || 0),
    createdAt: review.createdAt,
    updatedAt: review.updatedAt,
  };
}

function serializeGroupMessage(message, userMap, fileMap = new Map()) {
  const isAgent = message.senderType === "agent";
  const sender = isAgent
    ? {
      id: `agent:${message.agentId}`,
      name: message.agentName || "公司智能体",
      avatarUrl: null,
      kind: "agent",
    }
    : serializeGroupMember(userMap.get(message.senderUserId));
  return {
    id: message.id,
    groupId: message.groupId,
    senderUserId: message.senderUserId,
    sender,
    senderType: isAgent ? "agent" : "user",
    agent: isAgent ? {
      id: message.agentId,
      label: message.agentName || "公司智能体",
      iconSrc: message.agentIconSrc || null,
      status: message.agentStatus || "completed",
    } : null,
    content: message.content,
    citations: Array.isArray(message.citations) ? message.citations : [],
    triggeredByMessageId: message.triggeredByMessageId || null,
    attachments: (Array.isArray(message.attachmentIds) ? message.attachmentIds : [])
      .map((fileId) => serializeGroupAttachment(fileMap.get(fileId)))
      .filter(Boolean),
    createdAt: message.createdAt,
    updatedAt: message.updatedAt,
  };
}

function parseStringList(value, allowedValues) {
  const values = Array.isArray(value)
    ? value
    : String(value || "").split(",");
  return [...new Set(values.map((item) => cleanText(item, 80)).filter((item) => allowedValues.has(item)))];
}

function detectSensitiveCategories(text) {
  const value = String(text || "");
  const detected = [];
  if (/(?:客户隐私|客户名单|联系方式|手机号|微信号)/u.test(value)) detected.push("客户隐私");
  if (/(?:合同|协议|签约|违约金)/u.test(value)) detected.push("合同");
  if (/(?:财务|发票|回款|银行|成本|利润)/u.test(value)) detected.push("财务");
  if (/(?:人事|薪资|绩效|离职|身份证)/u.test(value)) detected.push("人事");
  return detected;
}

function scanFileForMalware(filePath) {
  return new Promise((resolvePromise, rejectPromise) => {
    const scanner = process.env.MALWARE_SCANNER_COMMAND || "clamdscan";
    const child = spawn(scanner, ["--fdpass", "--no-summary", filePath], { stdio: ["ignore", "pipe", "pipe"] });
    const output = [];
    let settled = false;
    const finish = (error, value) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      if (error) rejectPromise(error);
      else resolvePromise(value);
    };
    const timer = setTimeout(() => {
      child.kill("SIGKILL");
      finish(Object.assign(new Error("文件安全检查超时，请稍后重试"), { statusCode: 503, code: "MALWARE_SCAN_TIMEOUT" }));
    }, 30_000);
    timer.unref();
    for (const stream of [child.stdout, child.stderr]) {
      stream.on("data", (chunk) => {
        if (output.reduce((total, item) => total + item.length, 0) < 8_192) output.push(chunk);
      });
    }
    child.on("error", (error) => {
      finish(Object.assign(new Error("文件安全检查服务尚未就绪"), { statusCode: 503, code: error?.code === "ENOENT" ? "MALWARE_SCANNER_MISSING" : "MALWARE_SCAN_FAILED" }));
    });
    child.on("close", (code) => {
      if (code === 0) finish(null, "clean");
      else if (code === 1) finish(Object.assign(new Error("文件未通过安全检查，已拒绝上传"), { statusCode: 403, code: "MALWARE_DETECTED" }));
      else finish(Object.assign(new Error("文件安全检查服务暂时不可用"), { statusCode: 503, code: "MALWARE_SCAN_FAILED" }));
    });
  });
}

function safeError(error) {
  if (error instanceof AuthError) return { status: error.statusCode, message: error.message, code: error.code };
  if (error instanceof UnsupportedDocumentError) return { status: 415, message: error.message, code: error.code };
  if (error?.code === "LIMIT_FILE_SIZE") return { status: 413, message: "文件不能超过 20MB", code: "FILE_TOO_LARGE" };
  if (error?.code === "AGENT_HARNESS_CONFLICT") {
    return { status: 409, message: "课件任务提交状态冲突，请重新发送本轮任务", code: "COURSEWARE_REQUEST_CONFLICT" };
  }
  if (error?.code === "PPTXGENJS_MISSING" || error?.code === "PPTXGENJS_UNAVAILABLE") {
    return { status: 503, message: "PPTX 生成能力暂时不可用，管理员已收到运行环境告警，请稍后重试", code: "COURSEWARE_RENDERER_UNAVAILABLE" };
  }
  if (["DOCUMENT_TITLE_REQUIRED", "DOCUMENT_TITLE_TOO_LONG", "DOCUMENT_CONTENT_REQUIRED", "DOCUMENT_CONTENT_TOO_LONG"].includes(error?.code)) {
    return { status: 400, message: error.message, code: error.code };
  }
  if (Number.isInteger(error?.statusCode)) return { status: error.statusCode, message: error.message, code: error.code || "REQUEST_FAILED" };
  return { status: 500, message: "任务处理失败，请稍后重试", code: "INTERNAL_ERROR" };
}

function cleanText(value, limit = 12_000) {
  return String(value || "").replace(/\s+/gu, " ").trim().slice(0, limit);
}

const IMAGE_FOLLOWUP_PROMPT = /^(?:(?:请)?(?:再|重新)?(?:帮我)?(?:做|生成|出|来)(?:一|1)?张(?:图|图片|海报)?|继续(?:做|生成|出图)?|重做|重新做|再换一个|换一个|换一张|换种风格|图呢)(?:[，,。；;！!\s].{0,240})?$/u;

export function resolveImageJobPrompt(prompt, priorMessages = []) {
  const currentPrompt = cleanText(prompt, 2_000);
  if (!currentPrompt || !IMAGE_FOLLOWUP_PROMPT.test(currentPrompt)) return currentPrompt;
  const previousPrompt = [...(Array.isArray(priorMessages) ? priorMessages : [])]
    .reverse()
    .find((message) => {
      if (message?.role !== "user" || (!message.imageModel && message.module !== "视觉作图")) return false;
      const content = cleanText(message.content, 2_000);
      return content.length >= 6 && !IMAGE_FOLLOWUP_PROMPT.test(content);
    });
  const inheritedPrompt = cleanText(previousPrompt?.content, 2_000);
  if (!inheritedPrompt) return currentPrompt;
  return [
    "延续上一轮图片需求：" + inheritedPrompt,
    "本轮调整：" + currentPrompt,
    "保持上一轮的主题、关键信息、日期、品牌和用途，生成一张不同构图的新图。",
  ].join("\n");
}

function parseKnowhowPage(html, fallbackTitle = "行业资料") {
  const source = String(html || "").slice(0, 1_200_000);
  const title = cleanText(source.match(/<title[^>]*>([\s\S]*?)<\/title>/iu)?.[1]?.replace(/<[^>]+>/gu, ""), 160) || fallbackTitle;
  const description = cleanText(source.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["'][^>]*>/iu)?.[1], 500)
    || cleanText(source.match(/<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["'][^>]*>/iu)?.[1], 500);
  const text = cleanText(source
    .replace(/<script[\s\S]*?<\/script>/giu, " ")
    .replace(/<style[\s\S]*?<\/style>/giu, " ")
    .replace(/<[^>]+>/gu, " "), 1_200);
  return { title, summary: description || text || `${title}（待人工补充摘要）` };
}

function validateKnowhowFetchUrl(value, { allowLocal = false } = {}) {
  let parsed;
  try { parsed = new URL(value); } catch { parsed = null; }
  if (!parsed || !["http:", "https:"].includes(parsed.protocol)) {
    throw Object.assign(new Error("采集源需要填写有效的 http 或 https 地址"), { statusCode: 400, code: "KNOWHOW_SOURCE_URL_INVALID" });
  }
  const hostname = parsed.hostname.toLowerCase();
  const localHost = ["localhost", "127.0.0.1", "::1"].includes(hostname)
    || hostname.endsWith(".local")
    || /^10\./u.test(hostname)
    || /^192\.168\./u.test(hostname)
    || /^172\.(?:1[6-9]|2\d|3[0-1])\./u.test(hostname)
    || hostname === "169.254.169.254";
  if (localHost && !allowLocal) {
    throw Object.assign(new Error("采集源不能指向本机或内网地址"), { statusCode: 400, code: "KNOWHOW_SOURCE_PRIVATE_URL" });
  }
  return parsed;
}

function selectChatHistorySnapshot(messages, {
  excludeHarnessRequestKey = "",
  excludeHarnessTaskId = "",
  excludeClientRequestId = "",
} = {}) {
  const selected = [];
  let renderedLength = 0;
  for (const prior of (Array.isArray(messages) ? messages : []).slice(-12).reverse()) {
    if (!["user", "assistant"].includes(prior.role)) continue;
    if (excludeHarnessRequestKey && prior.harnessRequestKey === excludeHarnessRequestKey) continue;
    if (excludeHarnessTaskId && prior.harnessTaskId === excludeHarnessTaskId) continue;
    if (excludeClientRequestId && prior.clientRequestId === excludeClientRequestId) continue;
    if (prior.route === "approval_required") continue;
    const content = cleanText(prior.content, 1_500);
    const lineLength = `${prior.role === "user" ? "员工" : "业务助手"}：${content}`.length;
    if (renderedLength + lineLength > 8_000) break;
    selected.unshift({
      id: String(prior.id || ""),
      role: prior.role,
      content,
    });
    renderedLength += lineLength;
  }
  return selected;
}

function renderChatHistoryLines(snapshot) {
  return snapshot.map((prior) => (
    `${prior.role === "user" ? "员工" : "业务助手"}：${redactSensitive(prior.content)}`
  ));
}

function cleanRelativeKnowledgePath(value) {
  const path = cleanText(value, 600).replace(/\\/gu, "/").replace(/^\/+/u, "");
  if (!path || path.split("/").some((part) => !part || part === "." || part === "..")) {
    throw Object.assign(new Error("知识库目标路径无效"), { statusCode: 400, code: "INVALID_KNOWLEDGE_PATH" });
  }
  return path;
}

function sessionToken(req) {
  return req.cookies?.[COOKIE_NAME] || parseBearerToken(req.headers.authorization) || null;
}

async function ensureInitialData(store) {
  await store.transaction((draft) => {
    const defaults = [
      { code: "sales", name: "销售部" },
      { code: "courseware", name: "课程组" },
      { code: "content", name: "内容组" },
    ];
    const timestamp = new Date().toISOString();
    for (const department of defaults) {
      if (!draft.departments.some((item) => item.code === department.code)) {
        draft.departments.push({ id: randomUUID(), ...department, status: "active", createdAt: timestamp, updatedAt: timestamp });
      }
    }
    if (!draft.modelPolicies.some((policy) => policy.key === "default")) {
      draft.modelPolicies.push({
        id: randomUUID(),
        key: "default",
        defaultModel: DEFAULT_TEXT_MODEL_ID,
        allowEmployeeSwitch: true,
        dailyUserLimit: 100,
        enabled: true,
        createdAt: timestamp,
        updatedAt: timestamp,
      });
    }
  });
}

async function bootstrapFromEnvironment(auth) {
  const email = process.env.BOOTSTRAP_ADMIN_EMAIL?.trim();
  const displayName = process.env.BOOTSTRAP_ADMIN_NAME?.trim();
  const password = process.env.BOOTSTRAP_ADMIN_PASSWORD;
  const users = await auth.store.listUsers();
  if (users.length || !email || !displayName || !password) return false;
  await auth.bootstrapFirstAdmin({ email, displayName, password });
  return true;
}

function buildKnowledgeDocuments(documents, chunks) {
  const byDocument = new Map();
  for (const chunk of chunks) {
    const list = byDocument.get(chunk.documentId) || [];
    list.push({ id: chunk.id, index: chunk.position, content: chunk.content });
    byDocument.set(chunk.documentId, list);
  }
  return documents.map((document) => ({ ...document, chunks: byDocument.get(document.id) || [] }));
}

function filterKnowledgeScope(documents, scope, user) {
  if (scope === "智能匹配") return documents;
  if (scope === "我的资料") return documents.filter((document) => document.ownerUserId === user.id || document.ownerId === user.id);
  return documents.filter((document) => document.space === scope
    || document.space === "公司通用知识"
    || (Array.isArray(document.spaces) && (document.spaces.includes(scope) || document.spaces.includes("公司通用知识"))));
}

function normalizeContentTopic(input) {
  const topic = {
    title: redactSensitive(cleanText(input?.title, 160)),
    corePoint: redactSensitive(cleanText(input?.corePoint, 1_000)),
    audience: redactSensitive(cleanText(input?.audience, 160)),
  };
  if (!topic.title) throw Object.assign(new Error("请填写选题标题"), { statusCode: 400, code: "TOPIC_TITLE_REQUIRED" });
  if (containsRestrictedBusinessContent(`${topic.title}\n${topic.corePoint}\n${topic.audience}`)) {
    throw Object.assign(new Error("该选题包含不在员工工作台范围内的客户项目内容"), { statusCode: 403, code: "CONTENT_SCOPE_DENIED" });
  }
  return topic;
}

function serializeContentTopic(topic) {
  return {
    id: topic.id,
    title: topic.title,
    corePoint: topic.corePoint || "",
    audience: topic.audience || "",
    status: topic.status,
    visibility: CONTENT_TOPIC_VISIBILITY,
    createdAt: topic.createdAt,
    updatedAt: topic.updatedAt,
  };
}

function coursewareText(value, limit = 24_000) {
  return redactSensitive(String(value || ""))
    .normalize("NFKC")
    .replace(/\r\n?/gu, "\n")
    .replace(/<[^>]*>/gu, " ")
    .replace(/(?:https?|ftp|file|data|javascript):\/\/\S+/giu, " ")
    .replace(/[ \t]+/gu, " ")
    .replace(/\n{3,}/gu, "\n\n")
    .trim()
    .slice(0, limit);
}

function splitCoursewarePoints(value) {
  return String(value || "")
    .split(/(?:\n+|\s+-\s+|(?<=[。！？；!?;])\s*)/u)
    .map((item) => item.replace(/^[-*•\d.、)）\s]+/u, "").trim())
    .filter(Boolean)
    .map((item) => item.slice(0, 220));
}

function splitCoursewarePageBoundary(value) {
  return String(value || "").replace(
    /(?<![#\n])\s+(?=(?:#{1,4}\s*)?第\s*\d+\s*页(?:\s|[｜|:：-]))/gu,
    "\n\n",
  );
}

function parseCoursewarePageHeading(line) {
  const match = String(line || "").match(
    /^(?:#{1,4}\s*)?第\s*(\d+)\s*页(?:\s*[｜|:：-]\s*|\s+)(.+)$/u,
  );
  if (!match) return null;
  const segments = match[2].split(/\s+-\s+/u).map((item) => item.trim()).filter(Boolean);
  const pageTitle = segments.shift() || `第${match[1]}页`;
  return {
    title: pageTitle.slice(0, 72),
    points: splitCoursewarePoints(segments.join("\n")),
  };
}

const GENERIC_COURSEWARE_REQUEST = /^(?:(?:请|麻烦|帮我|给我|做|制作|生成|输出|导出|来|弄|搞|想要|需要|一个|一份|一套|的|成|为)\s*)*(?:(?:pptx?|课件|幻灯片)\s*){1,2}(?:格式|文件)?[。.!！?？]*$/iu;
const COURSEWARE_CLARIFICATION_DRAFT = /(?:还缺|需要|请).{0,24}(?:补充|确认|说明|提供).{0,36}(?:主题|受众|对象|用途|场景|内容|大纲)/iu;

export function coursewareBriefNeedsClarification({ prompt, history = [], attachmentCount = 0 } = {}) {
  if (Number(attachmentCount) > 0) return false;
  const current = coursewareText(prompt, 300).replace(/\s+/gu, "");
  const currentIsGeneric = current.length < 5 || GENERIC_COURSEWARE_REQUEST.test(current);
  if (!currentIsGeneric) return false;
  const priorSpecificRequest = (Array.isArray(history) ? history : []).some((item) => {
    if (item?.role !== "user") return false;
    const text = coursewareText(item?.content, 600).replace(/\s+/gu, "");
    return text.length >= 8 && !GENERIC_COURSEWARE_REQUEST.test(text);
  });
  return !priorSpecificRequest;
}

export function buildCoursewareSpec({ title, content, author }) {
  const safeTitle = coursewareText(title, 80) || "内部课程课件";
  const safeContent = coursewareText(content);
  if (!safeContent) throw Object.assign(new Error("请先生成或填写课件内容"), { statusCode: 400, code: "COURSEWARE_CONTENT_REQUIRED" });
  if (coursewareBriefNeedsClarification({ prompt: safeTitle }) || COURSEWARE_CLARIFICATION_DRAFT.test(safeContent)) {
    throw Object.assign(new Error("还缺少 PPT 主题、受众或用途，请补充后再生成"), {
      statusCode: 400,
      code: "COURSEWARE_BRIEF_INCOMPLETE",
    });
  }
  if (containsRestrictedBusinessContent(`${safeTitle}\n${safeContent}`)) {
    throw Object.assign(new Error("课件内容包含不在员工工作台范围内的客户项目资料"), { statusCode: 403, code: "COURSEWARE_SCOPE_DENIED" });
  }

  const lines = splitCoursewarePageBoundary(safeContent).split("\n").map((line) => line.trim()).filter(Boolean);
  const sections = [];
  let current = null;
  for (const line of lines) {
    const pageHeading = parseCoursewarePageHeading(line);
    if (pageHeading) {
      current = pageHeading;
      sections.push(current);
      continue;
    }
    const heading = line.match(/^#{1,4}\s+(.+)$/u);
    if (heading) {
      current = { title: heading[1].trim().slice(0, 72), points: [] };
      sections.push(current);
      continue;
    }
    if (!current) {
      current = { title: "核心内容", points: [] };
      sections.push(current);
    }
    current.points.push(...splitCoursewarePoints(line));
  }

  let contentSections = sections
    .map((section) => ({ ...section, points: section.points.filter(Boolean).slice(0, 8) }))
    .filter((section) => section.points.length);
  if (contentSections.length < 3) {
    const allPoints = splitCoursewarePoints(safeContent).slice(0, 36);
    const pageCount = Math.max(3, Math.min(8, Math.ceil(allPoints.length / 4)));
    const titles = ["核心判断", "关键方法", "执行步骤", "常见问题", "实战要点", "复盘标准", "团队协同", "下一步行动"];
    contentSections = Array.from({ length: pageCount }, (_, index) => ({
      title: titles[index],
      points: allPoints.filter((_item, pointIndex) => pointIndex % pageCount === index).slice(0, 8),
    })).filter((section) => section.points.length);
  }
  contentSections = contentSections.slice(0, 10);

  const layouts = ["statement", "split-left", "four-quadrant", "timeline-horizontal", "process-chevron", "staircase", "comparison", "checklist", "problem-solution", "hub-spoke"];
  const slides = [
    {
      title: safeTitle,
      body: ["围绕真实工作场景，形成可理解、可执行、可复盘的行动方法。"],
      layoutType: "cover",
      notes: "开场说明本次课程目标、适用岗位和预期成果。",
    },
    ...contentSections.map((section, index) => ({
      title: section.title || `课程要点 ${index + 1}`,
      body: section.points.length ? section.points : ["结合实际工作补充本页内容。"],
      layoutType: layouts[index],
      notes: `讲解本页的${section.title || "核心内容"}，结合公司真实工作场景说明，并在结束前确认学员理解。`,
    })),
    {
      title: "把方法落实到下一次工作",
      body: ["明确一个今天执行的动作", "确定负责人和完成时间", "记录结果并在团队内复盘"],
      layoutType: "closing",
      notes: "请学员写下一个立即执行的动作，并约定复盘时间。",
    },
  ];
  return {
    title: safeTitle,
    subtitle: "创创内部工作方法课",
    author: coursewareText(author, 60) || "创创",
    filename: safeTitle,
    slides,
  };
}

function buildCoursewareAction({ title, harnessTaskId, content }) {
  return {
    contract: "workspace-courseware-action.v1",
    type: "export_courseware",
    label: "导出 PPT",
    title: coursewareText(title, 80) || "内部课程课件",
    harnessTaskId: cleanText(harnessTaskId, 80) || null,
    contentSha256: sha256Summary(content),
  };
}

function canAccessFile(file, user) {
  if (!file || !user) return false;
  if (user.role === "admin" || file.ownerUserId === user.id) return true;
  return user.role === "manager"
    && file.scope === "department"
    && file.departmentId
    && file.departmentId === user.departmentId;
}

function canExportCrm(user) {
  return user?.role === "admin"
    && Array.isArray(user?.permissions)
    && user.permissions.includes("crm_export_filtered");
}

export async function createApp(options = {}) {
  const dataDir = resolve(options.dataDir || process.env.WORKSPACE_DATA_DIR || join(projectRoot, "data"));
  const uploadDir = join(dataDir, "uploads");
  const autoCutUploadDir = join(uploadDir, "auto-cut-sources");
  const artifactDir = join(dataDir, "artifacts");
  const weComContentDir = join(dataDir, "wecom-content");
  const lovartExecutorUploadDir = join(uploadDir, "lovart-executor");
  const weComBootstrapTokenPath = join(dataDir, ".wecom-bootstrap-token");
  const weComBootstrapPendingPath = join(dataDir, ".wecom-bootstrap-pending.json");
  await mkdir(uploadDir, { recursive: true, mode: 0o700 });
  await mkdir(autoCutUploadDir, { recursive: true, mode: 0o700 });
  await mkdir(artifactDir, { recursive: true, mode: 0o700 });
  await mkdir(weComContentDir, { recursive: true, mode: 0o700 });
  await mkdir(lovartExecutorUploadDir, { recursive: true, mode: 0o700 });
  const store = options.store || createStore({ filePath: join(dataDir, "state.json") });
  const wechatRuns = options.wechatRunStore || createWechatRunStore({ store });
  const autoCutRuns = options.autoCutRunStore || createAutoCutRunStore({ filePath: join(dataDir, "auto-cut-runs.json") });
  const geoStore = options.geoStore || createGeoStore({
    filePath: join(dataDir, "geo-state.json"),
    evidenceDir: join(dataDir, "geo-evidence"),
  });
  const workspaceObjectStore = options.workspaceObjectStore || createWorkspaceObjectStore({ env: process.env });
  const workspaceObjectStorageEnabled = workspaceObjectStore?.config?.enabled === true;
  const geoExternalConnectorsEnabled = options.geoExternalConnectorsEnabled
    ?? String(process.env.GEO_LIVE_API_ENABLED || "false") === "true";
  const geoConnectorRegistry = options.geoConnectorRegistry || createGeoConnectorRegistry({
    env: process.env,
    fetchImpl: options.geoFetch || fetch,
    now: options.geoNow || (() => new Date()),
  });
  const geoBudgetGuard = options.geoBudgetGuard || createGeoBudgetGuard({
    dailyLimitFen: process.env.GEO_DAILY_BUDGET_FEN,
    monthlyLimitFen: process.env.GEO_MONTHLY_BUDGET_FEN,
    estimatedCostFen: process.env.GEO_COST_PER_OBSERVATION_FEN,
    now: options.geoNow || (() => new Date()),
    loadUsage: async ({ dateKey, monthKey }) => {
      const events = await geoStore.listAudit();
      return events.reduce((totals, event) => {
        if (event.action !== "geo.run.completed" || event.metadata?.dataMode !== "live_api") return totals;
        const chargedFen = Math.max(0, Math.trunc(Number(event.metadata?.chargedFen) || 0));
        const createdAt = String(event.createdAt || "");
        if (createdAt.startsWith(dateKey)) totals.dailyFen += chargedFen;
        if (createdAt.startsWith(monthKey)) totals.monthlyFen += chargedFen;
        return totals;
      }, { dailyFen: 0, monthlyFen: 0 });
    },
  });
  const geoService = options.geoService || createGeoService({
    geoStore,
    now: options.geoNow || (() => new Date()),
    externalConnectorsEnabled: geoExternalConnectorsEnabled,
    connectorRegistry: geoConnectorRegistry,
    budgetGuard: geoBudgetGuard,
  });
  const chatProgressHub = options.chatProgressHub || createChatProgressHub();
  await store.init();
  await wechatRuns.init?.();
  await autoCutRuns.init();
  await ensureInitialData(store);
  await geoService.bootstrapDemo();
  const statfsImpl = options.statfs || statfs;
  const diagnosticLog = options.diagnosticLog || ((entry) => {
    const line = JSON.stringify(entry);
    if (entry.level === "error") console.error(line);
    else if (entry.level === "warn") console.warn(line);
    else console.info(line);
  });
  const lifecycleState = { shuttingDown: false };
  const logDiagnostic = (level, event, fields = {}) => {
    try {
      diagnosticLog({
        timestamp: new Date().toISOString(),
        level,
        event,
        service: "employee-workspace",
        ...fields,
      });
    } catch {
      // Diagnostics must never interrupt a business request.
    }
  };
  const imageInitialCredits = Math.max(0, Math.trunc(Number(
    options.imageInitialCredits ?? process.env.IMAGE_INITIAL_CREDITS ?? 20,
  ) || 0));
  const imageCreditCost = 1;
  const rechargeRequested = options.rechargeEnabled === true
    || String(process.env.RECHARGE_ENABLED || "false") === "true";
  const rechargeGateway = options.rechargeGateway || createRechargeGateway({ env: process.env, enabled: rechargeRequested });
  const rechargeEnabled = rechargeGateway.status.enabled === true;
  const lovart = options.lovartClient || createLovartClient({
    accessKey: options.lovartAccessKey,
    secretKey: options.lovartSecretKey,
    projectId: options.lovartProjectId,
    scriptPath: options.lovartSkillPath,
    homeDir: join(dataDir, ".lovart"),
  });
  const imageClient = options.imageClient
    || options.libImageClient
    || (options.lovartClient ? options.lovartClient : createLibImageClient({
      baseUrl: options.libImageBaseUrl,
      endpoint: options.libImageEndpoint,
      apiKey: options.libImageApiKey,
      model: options.libImageModel,
      cliPath: options.libImageCliPath,
      cliProtocol: options.libImageCliProtocol,
      libTvProjectId: options.libTvProjectId,
      timeoutMs: options.libImageTimeoutMs,
    }));
  const brandGeneratedImageImpl = options.brandGeneratedImage || brandGeneratedImage;
  const imageBrandLogoPath = resolve(
    options.imageBrandLogoPath
      || process.env.IMAGE_BRAND_LOGO_PATH
      || join(projectRoot, "web/public/chuang-logo-white.png"),
  );
  const lovartExecutorMode = String(
    options.lovartExecutorMode ?? process.env.LOVART_EXECUTOR_MODE ?? "",
  ).trim();
  const lovartExecutorSecret = String(
    options.lovartExecutorSecret ?? process.env.LOVART_EXECUTOR_SECRET ?? "",
  ).trim();
  const lovartRemoteEnabled = lovartExecutorMode === "manager-poll" && lovartExecutorSecret.length >= 32;
  const remoteImageProvider = cleanText(
    options.imageExecutorProvider ?? process.env.IMAGE_EXECUTOR_PROVIDER ?? "lovart",
    40,
  ) || "lovart";
  const imageProvider = lovartRemoteEnabled
    ? remoteImageProvider
    : imageClient.provider || (options.lovartClient ? "lovart" : "lib_image");
  const libTvImageEnabled = imageClient.configured === true || lovartRemoteEnabled;
  const imageEnabled = libTvImageEnabled;
  const imageRemoteJobTimeoutMs = Math.max(1_000, Number(
    options.imageRemoteJobTimeoutMs
      ?? process.env.IMAGE_REMOTE_JOB_TIMEOUT_MS
      ?? 5 * 60 * 1000,
  ) || 5 * 60 * 1000);
  const imageRemoteJobWatchdogIntervalMs = Math.max(250, Number(
    options.imageRemoteJobWatchdogIntervalMs
      ?? process.env.IMAGE_REMOTE_JOB_WATCHDOG_INTERVAL_MS
      ?? 15 * 1000,
  ) || 15 * 1000);
  const interruptedImageJobs = (await store.listImageJobs()).filter((job) => ["queued", "running"].includes(job.status));
  for (const job of interruptedImageJobs) {
    await store.failImageJobAndRefund(job.id, {
      errorCode: "IMAGE_JOB_INTERRUPTED",
      errorMessage: "服务重启导致任务中断，积分已自动退回",
    });
  }
  const auth = options.auth || createAuth({
    store,
    wechatRuns,
    sessionTtlMs: numberFromEnv("SESSION_TTL_HOURS", 12) * 60 * 60 * 1000,
  });
  const localPreviewAuthEnabled = options.localPreviewAuth === true
    || (options.localPreviewAuth !== false
      && process.env.NODE_ENV !== "production"
      && process.env.LOCAL_PREVIEW_AUTH === "true");
  const wecomConfig = {
    corpId: String(options.wecomConfig?.corpId ?? process.env.WECOM_CORP_ID ?? "").trim(),
    agentId: String(options.wecomConfig?.agentId ?? process.env.WECOM_AGENT_ID ?? "").trim(),
    secret: String(options.wecomConfig?.secret ?? process.env.WECOM_APP_SECRET ?? "").trim(),
    redirectUri: String(options.wecomConfig?.redirectUri ?? process.env.WECOM_REDIRECT_URI ?? "").trim(),
    autoProvision: options.wecomConfig?.autoProvision ?? String(process.env.WECOM_AUTO_PROVISION || "true") !== "false",
  };
  const wecomRequested = Boolean(options.wecomClient)
    || Object.values(wecomConfig).slice(0, 4).some(Boolean)
    || String(process.env.WECOM_ENABLED || "false") === "true";
  const wecomClient = options.wecomClient || (wecomRequested ? createWeComClient(wecomConfig) : null);
  const wecomCallbackConfig = {
    enabled: options.wecomCallbackConfig?.enabled
      ?? String(process.env.WECOM_CALLBACK_ENABLED || "false") === "true",
    token: String(options.wecomCallbackConfig?.token ?? process.env.WECOM_CALLBACK_TOKEN ?? "").trim(),
    encodingAesKey: String(options.wecomCallbackConfig?.encodingAesKey ?? process.env.WECOM_CALLBACK_AES_KEY ?? "").trim(),
    bridgeToken: String(options.wecomCallbackConfig?.bridgeToken ?? process.env.WECOM_CONTENT_BRIDGE_TOKEN ?? "").trim(),
  };
  const wecomContentEnabled = Boolean(
    wecomCallbackConfig.enabled
    && wecomClient
    && wecomConfig.corpId
    && wecomCallbackConfig.token
    && wecomCallbackConfig.encodingAesKey
    && wecomCallbackConfig.bridgeToken.length >= 32,
  );
  const wecomCallbackCrypto = wecomContentEnabled ? createWeComCallbackCrypto({
    token: wecomCallbackConfig.token,
    encodingAesKey: wecomCallbackConfig.encodingAesKey,
    receiveId: wecomConfig.corpId,
  }) : null;
  const wecomContentTasks = options.wecomContentTaskStore || (wecomContentEnabled
    ? createWeComContentTaskStore({ filePath: join(dataDir, "wecom-content-tasks.json") })
    : null);
  if (wecomContentTasks?.init) await wecomContentTasks.init();
  const requestedWecomDirectorySyncIntervalMs = Number(
    options.wecomDirectorySyncIntervalMs || process.env.WECOM_DIRECTORY_SYNC_INTERVAL_MS || 10 * 60 * 1000,
  );
  const wecomDirectorySyncIntervalMs = Number.isFinite(requestedWecomDirectorySyncIntervalMs)
    ? Math.max(5 * 60 * 1000, requestedWecomDirectorySyncIntervalMs)
    : 10 * 60 * 1000;
  let wecomDirectorySyncPromise = null;
  let wecomDirectorySyncTimer = null;
  let wecomDirectoryState = {
    enabled: Boolean(wecomClient && typeof wecomClient.listDirectory === "function"),
    status: "idle",
    lastAttemptAt: null,
    lastSuccessAt: null,
    count: 0,
    errorCode: null,
  };

  async function syncWeComDirectory({ force = false } = {}) {
    if (!wecomDirectoryState.enabled) return { ...wecomDirectoryState };
    if (wecomDirectorySyncPromise) return wecomDirectorySyncPromise;
    const existingUsers = await store.listUsers();
    if (!existingUsers.length) {
      wecomDirectoryState = { ...wecomDirectoryState, status: "awaiting_admin" };
      return { ...wecomDirectoryState };
    }
    const lastSuccessAt = Date.parse(wecomDirectoryState.lastSuccessAt || "");
    if (!force && Number.isFinite(lastSuccessAt) && Date.now() - lastSuccessAt < wecomDirectorySyncIntervalMs) {
      return { ...wecomDirectoryState };
    }
    wecomDirectorySyncPromise = (async () => {
      wecomDirectoryState = {
        ...wecomDirectoryState,
        status: "syncing",
        lastAttemptAt: new Date().toISOString(),
        errorCode: null,
      };
      try {
        const directory = await wecomClient.listDirectory();
        const stats = await auth.syncWeComDirectory(directory);
        wecomDirectoryState = {
          enabled: true,
          status: "ready",
          lastAttemptAt: wecomDirectoryState.lastAttemptAt,
          lastSuccessAt: stats.syncedAt,
          count: stats.active,
          errorCode: null,
          stats,
        };
      } catch (error) {
        wecomDirectoryState = {
          ...wecomDirectoryState,
          status: "error",
          errorCode: error?.code || "WECOM_DIRECTORY_SYNC_FAILED",
        };
        console.error("wecom_directory_sync_failed", { code: wecomDirectoryState.errorCode });
      } finally {
        wecomDirectorySyncPromise = null;
      }
      return { ...wecomDirectoryState };
    })();
    return wecomDirectorySyncPromise;
  }
  const allowEnvironmentBootstrap = options.bootstrapEnvironment === true
    || process.env.ALLOW_BOOTSTRAP_ADMIN_FROM_ENV === "true";
  if (allowEnvironmentBootstrap) await bootstrapFromEnvironment(auth);
  const generate = options.generateAnswer || generateAnswer;
  const generateAutoCutPlan = options.generateAutoCutPlan || createAutoCutMiniMaxPlanner();
  const executeAutoCutRun = options.executeAutoCutRun || (async ({ run, onStage, artifactDir: targetArtifactDir, generate: generateModel }) => executeAutoCutWorkflow({
    run,
    onStage,
    artifactDir: targetArtifactDir,
    generate: generateModel,
    sourceUrl: run.source?.type === "oss" ? await getAutoCutOssSignedFileUrl(run.source.path, AUTO_CUT_OSS_CONFIG) : null,
  }));
  const agentHarness = options.agentHarness || createAgentHarnessClient();
  const langGraphAdapter = options.langGraphAdapter || createLangGraphAdapter({
    enabled: String(process.env.LANGGRAPH_ENABLED || "false") === "true",
    executionLeaseClient: agentHarness,
  });
  const executionRecoveryHooks = options.executionRecoveryHooks || {};
  const groupAgentTriggerGuard = createGroupAgentTriggerGuard({
    repeatWindowMs: numberFromEnv("GROUP_AGENT_REPEAT_WINDOW_MS", 30_000),
    windowMs: numberFromEnv("GROUP_AGENT_RATE_WINDOW_MS", 60_000),
    limit: numberFromEnv("GROUP_AGENT_RATE_LIMIT", 4),
  });
  const scanUpload = options.scanUploadedFile || scanFileForMalware;
  const requireMalwareScan = options.requireMalwareScan ?? String(process.env.REQUIRE_MALWARE_SCAN || "true") !== "false";
  const knowhowAutoCollect = options.knowhowAutoCollect ?? String(process.env.KNOWHOW_AUTO_COLLECT || "true") === "true";
  const knowhowCollectionIntervalMs = Math.max(60_000, Number(options.knowhowCollectionIntervalMs || process.env.KNOWHOW_COLLECTION_INTERVAL_MS || 30 * 60 * 1000));
  const knowhowModelId = DEFAULT_KNOWHOW_MODEL_ID;
  const knowhowModel = options.knowhowModel || createPublicSourceKnowhowSearch({
    fetchImpl: options.knowhowFetch || fetch,
    generateAnswer: generate,
    modelId: knowhowModelId,
  });
  const crm = createCrm({ store });
  const crmReadonly = options.crmReadonlyClient || createCrmReadonlyClient();
  const crmOutreach = options.crmOutreachClient || createCrmOutreachClient();
  const outreachReviewIngestSecret = String(
    options.outreachReviewIngestSecret ?? process.env.OUTREACH_REVIEW_INGEST_SECRET ?? "",
  ).trim();
  const workspacePublicUrl = String(
    options.workspacePublicUrl ?? process.env.WORKSPACE_PUBLIC_URL ?? "https://sail-crm.chuangchuang.cn/workspace/",
  ).trim();
  let outreachMaintenanceTimer = null;
  let imageRemoteJobWatchdogTimer = null;
  let imageReferenceCacheTimer = null;
  let knowhowCollectionTimer = null;
  let knowhowCollectionPromise = null;
  let knowhowCollectorState = {
    enabled: knowhowAutoCollect,
    running: false,
    modelId: knowhowModelId,
    intervalMs: knowhowCollectionIntervalMs,
    lastRunAt: null,
    nextRunAt: knowhowAutoCollect ? new Date(Date.now() + knowhowCollectionIntervalMs).toISOString() : null,
    lastStatus: "idle",
  };
  const externalKnowledge = options.externalKnowledgeClient || createExternalKnowledgeClient();
  const knowledgeSyncSecret = String(
    options.knowledgeSyncSecret
      ?? process.env.KNOWLEDGE_SYNC_SHARED_SECRET
      ?? process.env.HERMES_BRIDGE_SHARED_SECRET
      ?? "",
  );
  const knowledgeSyncBridgeAuth = options.knowledgeSyncBridgeAuth || (async (supplied) => {
    if (!supplied) return false;
    const bridgeBase = String(process.env.HERMES_BRIDGE_URL || "").trim();
    if (!bridgeBase) return false;
    try {
      const bridgeUrl = new URL("/knowledge-sync/snapshot?library=team-workspace", bridgeBase);
      if (!["127.0.0.1", "localhost", "::1"].includes(bridgeUrl.hostname)) return false;
      const response = await fetch(bridgeUrl, {
        headers: { "x-hermes-bridge-secret": supplied },
        signal: AbortSignal.timeout(5_000),
      });
      await response.body?.cancel().catch(() => {});
      return response.ok;
    } catch {
      return false;
    }
  });
  const activeArtifactUsers = new Set();
  const activeImageUsers = new Set();
  const activeImageJobs = new Set();
  const activeConversationRequests = new Map();
  const activeUploadUsers = new Set();
  let activeUploadCount = 0;
  let reservedStorageBytes = 0;
  const reservedStorageByUser = new Map();
  let storageReservationTail = Promise.resolve();

  function normalizeOutreachReviewInput(input = {}) {
    const sourceKey = cleanText(input.sourceKey || input.idempotencyKey, 200);
    const crmCustomerId = cleanText(input.crmCustomerId, 200);
    const customerName = cleanText(input.customerName || input.customerNameMasked, 200);
    const ownerName = cleanText(input.ownerName, 200);
    const content = cleanText(input.content, 4_000);
    if (input.eligible !== true) {
      throw Object.assign(new Error("只有已通过自动筛选的客户可以进入审核箱"), { statusCode: 409, code: "OUTREACH_NOT_ELIGIBLE" });
    }
    if (!sourceKey || !crmCustomerId || !customerName || !ownerName || !content) {
      throw Object.assign(new Error("触达审核信息不完整"), { statusCode: 400, code: "OUTREACH_REVIEW_INCOMPLETE" });
    }
    if (/承诺收益|保证成交|稳赚/iu.test(content)) {
      throw Object.assign(new Error("拟发送内容包含禁止承诺"), { statusCode: 409, code: "OUTREACH_FORBIDDEN_CLAIM" });
    }
    return {
      sourceKey,
      source: cleanText(input.source || "CRM自动化", 120),
      campaignName: cleanText(input.campaignName || "客户经营触达", 160),
      crmCustomerId,
      crmOutreachTaskId: cleanText(input.crmOutreachTaskId, 300) || null,
      customerName,
      customerNameMasked: cleanText(input.customerNameMasked, 200) || null,
      contactMasked: cleanText(input.contactMasked, 120) || null,
      ownerName,
      crmWorkerId: cleanText(input.crmWorkerId, 200) || null,
      externalUserId: cleanText(input.externalUserId, 200) || null,
      reason: cleanText(input.reason || "CRM自动分析建议触达", 500),
      evidence: Array.isArray(input.evidence) ? input.evidence.map((item) => cleanText(item, 300)).filter(Boolean).slice(0, 12) : [],
      riskLevel: ["low", "medium", "high"].includes(input.riskLevel) ? input.riskLevel : "low",
      riskChecks: Array.isArray(input.riskChecks) ? input.riskChecks.map((item) => cleanText(item, 200)).filter(Boolean).slice(0, 20) : [],
      content,
      tags: Array.isArray(input.tags) ? input.tags.map((item) => cleanText(item, 80)).filter(Boolean).slice(0, 20) : [],
      autoSubmitAfterApproval: true,
      requestedAt: new Date().toISOString(),
    };
  }

  async function notifyOutreachReviewAdmins(reviews) {
    if (!reviews.length || !wecomClient?.sendText) return { sent: 0, skipped: true };
    const admins = (await store.listUsers({ role: "admin" }))
      .filter((user) => user.status === "active" && user.wecomUserId);
    if (!admins.length) return { sent: 0, skipped: true };
    const preview = reviews.slice(0, 3).map((review, index) => (
      `${index + 1}. ${review.customerNameMasked || review.customerName}｜负责人 ${review.ownerName}｜${review.reason}`
    )).join("\n");
    const more = reviews.length > 3 ? `\n另有 ${reviews.length - 3} 条` : "";
    const approvalUrl = `${workspacePublicUrl.replace(/\/?$/u, "/")}?page=outreach-review`;
    const message = `CRM触达待你审核\n新增 ${reviews.length} 条\n${preview}${more}\n\n通过后自动提交企业微信；未通过不会触达。\n${approvalUrl}`;
    let sent = 0;
    for (const admin of admins) {
      try {
        await wecomClient.sendText(admin.wecomUserId, message);
        sent += 1;
      } catch (error) {
        await store.addAudit({
          action: "crm.outreach_review_notification_failed",
          actorUserId: null,
          targetType: "user",
          targetId: admin.id,
          metadata: { code: safeDiagnosticCode(error?.code || "WECOM_NOTIFY_FAILED") },
        }).catch(() => {});
      }
    }
    return { sent, skipped: false };
  }

  async function submitApprovedOutreachReview(review) {
    try {
      const result = await crmOutreach.submitApprovedReview(review);
      const deliveryStatus = result.status || "submitted";
      const updated = await store.updateOutreachReview(review.id, {
        status: deliveryStatus,
        deliveryStatus,
        crmOutreachTaskId: result.taskId || null,
        wecomTaskId: result.wecomTaskId || null,
        submittedAt: result.submittedAt || new Date().toISOString(),
        lastErrorCode: null,
        lastErrorMessage: null,
        nextRetryAt: null,
      });
      await store.addAudit({
        action: "crm.outreach_review_submitted",
        actorUserId: review.approvedByUserId || null,
        targetType: "outreach_review",
        targetId: review.id,
        metadata: { deliveryStatus, reused: result.reused === true },
      });
      return { review: updated, submitted: true };
    } catch (error) {
      const attempts = Number(review.submissionAttempts || 1);
      const retryable = attempts < 5;
      const nextRetryAt = retryable
        ? new Date(Date.now() + Math.min(30, 2 ** Math.max(0, attempts - 1)) * 60_000).toISOString()
        : null;
      const updated = await store.updateOutreachReview(review.id, {
        status: retryable ? "approved_retry_pending" : "manual_attention",
        deliveryStatus: retryable ? "retry_pending" : "manual_attention",
        lastErrorCode: safeDiagnosticCode(error?.code || "CRM_OUTREACH_FAILED"),
        lastErrorMessage: cleanText(error?.message || "触达任务提交失败", 500),
        nextRetryAt,
      });
      await store.addAudit({
        action: "crm.outreach_review_submission_failed",
        actorUserId: review.approvedByUserId || null,
        targetType: "outreach_review",
        targetId: review.id,
        metadata: { code: safeDiagnosticCode(error?.code || "CRM_OUTREACH_FAILED"), retryable },
      }).catch(() => {});
      return { review: updated, submitted: false, retryable };
    }
  }

  async function refreshOutreachDeliveryStatuses() {
    if (!crmOutreach.enabled) return;
    const tasks = await crmOutreach.taskStatuses();
    const byWeComId = new Map(tasks.filter((task) => task.wecomTaskId).map((task) => [String(task.wecomTaskId), task]));
    const byTaskId = new Map(tasks.filter((task) => task.taskId).map((task) => [String(task.taskId), task]));
    const reviews = await store.listOutreachReviews({ approvalStatus: "approved" });
    for (const review of reviews) {
      const task = byWeComId.get(String(review.wecomTaskId || "")) || byTaskId.get(String(review.crmOutreachTaskId || ""));
      if (!task?.status || task.status === review.deliveryStatus) continue;
      await store.updateOutreachReview(review.id, {
        status: task.status,
        deliveryStatus: task.status,
        deliveredAt: task.status === "sent" ? task.sentAt || new Date().toISOString() : review.deliveredAt || null,
      });
    }
  }

  async function maintainOutreachReviewQueue() {
    if (!crmOutreach.enabled) return;
    await refreshOutreachDeliveryStatuses().catch(() => {});
    const now = Date.now();
    const reviews = await store.listOutreachReviews({ approvalStatus: "approved" });
    for (const review of reviews) {
      if (review.deliveryStatus !== "retry_pending") continue;
      if (Date.parse(review.nextRetryAt || "") > now) continue;
      const claimed = await store.updateOutreachReview(review.id, {
        status: "submitting",
        deliveryStatus: "submitting",
        submissionAttempts: Number(review.submissionAttempts || 0) + 1,
        lastSubmissionAttemptAt: new Date().toISOString(),
      });
      if (claimed) await submitApprovedOutreachReview(claimed);
    }
  }

  async function reserveStorageCapacity(userId, incomingBytes = 0) {
    const operation = storageReservationTail.then(async () => {
      const files = await store.listFiles();
      const countedFiles = files.filter((file) => !["deleted", "failed", "quarantined"].includes(file.status));
      const userBytes = countedFiles
        .filter((file) => file.ownerUserId === userId)
        .reduce((total, file) => total + Number(file.size || 0), 0);
      const totalBytes = countedFiles.reduce((total, file) => total + Number(file.size || 0), 0);
      const userReservedBytes = reservedStorageByUser.get(userId) || 0;
      const userLimit = numberFromEnv("USER_STORAGE_LIMIT_MB", 500) * 1024 * 1024;
      const totalLimit = numberFromEnv("TOTAL_STORAGE_LIMIT_MB", 20_480) * 1024 * 1024;
      if (userBytes + userReservedBytes + incomingBytes > userLimit) {
        throw Object.assign(new Error("个人存储额度已用完，请清理资料或联系管理员"), { statusCode: 413, code: "USER_STORAGE_LIMIT" });
      }
      if (totalBytes + reservedStorageBytes + incomingBytes > totalLimit) {
        throw Object.assign(new Error("公司工作台存储空间已达上限，请联系管理员"), { statusCode: 507, code: "TOTAL_STORAGE_LIMIT" });
      }
      const disk = await statfsImpl(dataDir);
      const freeBytes = Number(disk.bavail) * Number(disk.bsize);
      const minimumFreeBytes = numberFromEnv("MIN_FREE_STORAGE_MB", 1024) * 1024 * 1024;
      if (!Number.isFinite(freeBytes) || freeBytes - reservedStorageBytes - incomingBytes < minimumFreeBytes) {
        throw Object.assign(new Error("服务器可用空间不足，已暂停生成和上传"), { statusCode: 507, code: "DISK_LOW_WATERMARK" });
      }
      const reservation = { id: randomUUID(), userId, bytes: incomingBytes };
      reservedStorageBytes += incomingBytes;
      reservedStorageByUser.set(userId, userReservedBytes + incomingBytes);
      return reservation;
    });
    storageReservationTail = operation.catch(() => {});
    return operation;
  }

  async function releaseStorageReservation(reservation) {
    if (!reservation) return;
    const operation = storageReservationTail.then(() => {
      reservedStorageBytes = Math.max(0, reservedStorageBytes - reservation.bytes);
      const remaining = Math.max(0, (reservedStorageByUser.get(reservation.userId) || 0) - reservation.bytes);
      if (remaining) reservedStorageByUser.set(reservation.userId, remaining);
      else reservedStorageByUser.delete(reservation.userId);
    });
    storageReservationTail = operation.catch(() => {});
    await operation;
  }

  async function ensureConversationCapacity(userId) {
    const conversations = await store.listConversations({ ownerUserId: userId });
    if (conversations.length >= 200) {
      throw Object.assign(new Error("历史对话已达 200 个，请先整理后再新建"), { statusCode: 409, code: "CONVERSATION_LIMIT" });
    }
  }

  const app = express();
  app.disable("x-powered-by");
  app.set("trust proxy", 1);
  app.use((req, res, next) => {
    const requestId = safeRequestId(req.get("x-request-id"));
    const startedAt = process.hrtime.bigint();
    req.workspaceRequestId = requestId;
    res.setHeader("x-request-id", requestId);
    res.once("finish", () => {
      const route = diagnosticRoute(req);
      if (route === "/workspace-api/healthz" && res.statusCode < 400) return;
      logDiagnostic(res.statusCode >= 500 ? "error" : res.statusCode >= 400 ? "warn" : "info", "request_completed", {
        requestId,
        method: req.method,
        route,
        statusCode: res.statusCode,
        durationMs: Number((Number(process.hrtime.bigint() - startedAt) / 1_000_000).toFixed(1)),
      });
    });
    next();
  });
  const secureCookies = String(process.env.SESSION_COOKIE_SECURE || "true") !== "false";
  const sessionCookieOptions = {
    httpOnly: true,
    secure: secureCookies,
    sameSite: "strict",
    path: "/workspace-api",
    maxAge: auth.sessionTtlMs,
  };
  const wecomStateCookieOptions = {
    httpOnly: true,
    secure: secureCookies,
    sameSite: "lax",
    path: "/workspace-api/auth/wecom",
    maxAge: 5 * 60 * 1000,
  };
  const setSessionCookie = (res, token) => res.cookie(COOKIE_NAME, token, sessionCookieOptions);
  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "blob:", "https://wework.qpic.cn"],
        connectSrc: ["'self'"],
        formAction: ["'self'", "https://openapi.alipay.com"],
      },
    },
  }));
  app.use(cookieParser());
  app.use(express.json({
    limit: "64kb",
    verify: (req, _res, buffer) => {
      if (String(req.originalUrl || "").startsWith("/workspace-api/payments/wechat/notify")) {
        req.rawBody = Buffer.from(buffer);
      }
    },
  }));
  app.use(express.urlencoded({ extended: false, limit: "64kb" }));
  app.use(express.text({ type: ["application/xml", "text/xml"], limit: "64kb" }));

  app.use((req, res, next) => {
    if (["GET", "HEAD", "OPTIONS"].includes(req.method) || !req.path.startsWith("/workspace-api/")) return next();
    const origin = req.headers.origin;
    if (!origin) return next();
    try {
      if (localPreviewAuthEnabled && ["127.0.0.1", "localhost", "::1"].includes(new URL(origin).hostname)) return next();
      const expected = `${req.protocol}://${req.get("host")}`;
      if (new URL(origin).origin !== expected) return res.status(403).json({ ok: false, error: "请求来源无效", code: "ORIGIN_DENIED" });
      return next();
    } catch {
      return res.status(403).json({ ok: false, error: "请求来源无效", code: "ORIGIN_DENIED" });
    }
  });

  const loginIpLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 100, keyGenerator: (req) => ipKeyGenerator(req.ip), standardHeaders: "draft-8", legacyHeaders: false });
  const loginLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    limit: 10,
    keyGenerator: (req) => `${ipKeyGenerator(req.ip)}:${cleanText(req.body?.email || req.body?.login, 240).toLowerCase() || "unknown"}`,
    standardHeaders: "draft-8",
    legacyHeaders: false,
  });
  const weComBootstrapLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    limit: 5,
    keyGenerator: (req) => ipKeyGenerator(req.ip),
    standardHeaders: "draft-8",
    legacyHeaders: false,
  });
  const chatLimiter = rateLimit({
    windowMs: 60 * 1000,
    limit: 12,
    keyGenerator: (req) => req.workspace?.user?.id || ipKeyGenerator(req.ip),
    standardHeaders: "draft-8",
    legacyHeaders: false,
  });
  const mutationLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    limit: 120,
    keyGenerator: (req) => req.workspace?.user?.id || "unauthenticated",
    standardHeaders: "draft-8",
    legacyHeaders: false,
  });
  const groupMessageLimiter = rateLimit({
    windowMs: 60 * 1000,
    limit: 60,
    keyGenerator: (req) => req.workspace?.user?.id || "unauthenticated",
    standardHeaders: "draft-8",
    legacyHeaders: false,
  });
  const uploadLimiter = rateLimit({
    windowMs: 24 * 60 * 60 * 1000,
    limit: 50,
    keyGenerator: (req) => req.workspace?.user?.id || "unauthenticated",
    standardHeaders: "draft-8",
    legacyHeaders: false,
  });
  const artifactLimiter = rateLimit({
    windowMs: 24 * 60 * 60 * 1000,
    limit: 20,
    keyGenerator: (req) => req.workspace?.user?.id || "unauthenticated",
    standardHeaders: "draft-8",
    legacyHeaders: false,
  });
  const upload = multer({
    dest: uploadDir,
    limits: { fileSize: 20 * 1024 * 1024, files: 1, fields: 8 },
    fileFilter(_req, file, callback) {
      file.originalname = normalizeUploadFilename(file.originalname);
      const extension = extname(file.originalname).toLowerCase();
      if (!ALLOWED_EXTENSIONS.has(extension)) return callback(Object.assign(new Error("暂不支持此文件类型"), { statusCode: 415 }));
      if (containsRestrictedBusinessContent(file.originalname) || isCustomerPathExcluded({ filePath: file.originalname })) {
        return callback(Object.assign(new Error("该资料不在员工办公知识范围内"), { statusCode: 403 }));
      }
      return callback(null, true);
    },
  });
  const autoCutVideoUpload = multer({
    dest: autoCutUploadDir,
    limits: {
      fileSize: numberFromEnv("AUTO_CUT_VIDEO_MAX_MB", 500) * 1024 * 1024,
      files: 1,
      fields: 1,
    },
    fileFilter(_req, file, callback) {
      file.originalname = normalizeUploadFilename(file.originalname);
      const extension = extname(file.originalname).toLowerCase();
      const mimeAllowed = AUTO_CUT_VIDEO_MIME_TYPES.has(file.mimetype) || file.mimetype === "application/octet-stream";
      if (!AUTO_CUT_VIDEO_EXTENSIONS.has(extension) || !mimeAllowed) {
        return callback(Object.assign(new Error("原片仅支持 MP4、MOV、M4V 或 WEBM 视频"), { statusCode: 415, code: "AUTO_CUT_VIDEO_REQUIRED" }));
      }
      if (containsRestrictedBusinessContent(file.originalname) || isCustomerPathExcluded({ filePath: file.originalname })) {
        return callback(Object.assign(new Error("该原片不在员工办公范围内"), { statusCode: 403, code: "AUTO_CUT_SOURCE_SCOPE_DENIED" }));
      }
      return callback(null, true);
    },
  });
  const lovartExecutorUpload = multer({
    dest: lovartExecutorUploadDir,
    limits: { fileSize: 25 * 1024 * 1024, files: 4, fields: 8 },
    fileFilter(_req, file, callback) {
      file.originalname = normalizeUploadFilename(file.originalname);
      const extension = extname(file.originalname).toLowerCase();
      if (!LOVART_IMAGE_MIME_BY_EXTENSION.has(extension)) {
        return callback(Object.assign(new Error("仅接受 Lovart 图片产物"), { statusCode: 415 }));
      }
      return callback(null, true);
    },
  });
  const wecomContentUpload = multer({
    dest: weComContentDir,
    limits: { fileSize: 20 * 1024 * 1024, files: 6, fields: 8 },
    fileFilter(_req, file, callback) {
      file.originalname = normalizeUploadFilename(file.originalname);
      const extension = extname(file.originalname).toLowerCase();
      const isVideo = file.fieldname === "video" && extension === ".mp4" && file.mimetype === "video/mp4";
      const isImage = file.fieldname === "images" && extension === ".png" && file.mimetype === "image/png";
      if (!isVideo && !isImage) {
        return callback(Object.assign(new Error("企业微信回传只接受单个 MP4 成片或最多 6 张 PNG 图片"), { statusCode: 415, code: "WECOM_ARTIFACT_REQUIRED" }));
      }
      return callback(null, true);
    },
  });

  const reserveUploadStorage = async (req, res, next) => {
    try {
      if (activeUploadUsers.has(req.workspace.user.id)) {
        throw Object.assign(new Error("已有资料正在上传，请等待完成后再试"), { statusCode: 409, code: "UPLOAD_ALREADY_RUNNING" });
      }
      if (activeUploadCount >= numberFromEnv("UPLOAD_CONCURRENCY_LIMIT", 5)) {
        throw Object.assign(new Error("当前上传任务较多，请稍后再试"), { statusCode: 429, code: "UPLOAD_CAPACITY_REACHED" });
      }
      activeUploadUsers.add(req.workspace.user.id);
      activeUploadCount += 1;
      const contentLength = Number(req.get("content-length"));
      const incomingBytes = Number.isFinite(contentLength) && contentLength > 0
        ? Math.min(contentLength, 22 * 1024 * 1024)
        : 20 * 1024 * 1024;
      let reservation;
      try {
        reservation = await reserveStorageCapacity(req.workspace.user.id, incomingBytes);
      } catch (error) {
        activeUploadUsers.delete(req.workspace.user.id);
        activeUploadCount = Math.max(0, activeUploadCount - 1);
        throw error;
      }
      let released = false;
      const release = () => {
        if (released) return;
        released = true;
        activeUploadUsers.delete(req.workspace.user.id);
        activeUploadCount = Math.max(0, activeUploadCount - 1);
        void releaseStorageReservation(reservation);
      };
      res.once("finish", release);
      res.once("close", release);
      next();
    } catch (error) { next(error); }
  };

  const requireUser = async (req, res, next) => {
    try {
      const token = sessionToken(req);
      if (!token) throw new AuthError("请先登录", "LOGIN_REQUIRED", 401);
      const authenticated = await auth.requireSession(token);
      req.workspace = { ...authenticated, token };
      next();
    } catch (error) { next(error); }
  };
  const requireAdmin = (req, _res, next) => {
    if (req.workspace?.user?.role !== "admin") return next(new AuthError("没有管理员权限", "FORBIDDEN", 403));
    next();
  };
  const requireKnowledgeReviewer = (req, _res, next) => {
    if (!["admin", "manager"].includes(req.workspace?.user?.role)) {
      return next(new AuthError("没有资料审核权限", "FORBIDDEN", 403));
    }
    next();
  };
  const activeWechatRuns = new Set();
  const executeWechatRun = options.executeWechatRun || executeWechatWorkflow;
  const planWechatWithHermes = options.planWechatWithHermes || createWechatHermesPlanner({ generateAnswer });
  const searchWechatKnowledge = options.searchWechatKnowledge;
  const generateWechatCover = options.generateWechatCover || (imageEnabled ? async ({ prompt, artifactDir: targetDir }) => {
    const generated = await imageClient.generateImage({
      prompt,
      model: "nano-banana-2",
      mode: "text-to-image",
      attachmentPaths: [],
      outputDir: targetDir,
    });
    const first = generated?.artifacts?.[0];
    if (!first?.path) throw Object.assign(new Error("封面生成服务没有返回图片"), { code: "COVER_PROVIDER_EMPTY" });
    return first;
  } : undefined);
  const wechatArtifactDir = join(artifactDir, "wechat-editor");

  function publicWechatWorkflow(workflow) {
    return {
      id: cleanText(workflow?.id || "wechat-default", 160),
      name: cleanText(workflow?.name || "公众号生产默认流程", 160),
      version: cleanText(workflow?.version || "v1", 80),
      status: cleanText(workflow?.status || "active", 40),
      nodes: Array.isArray(workflow?.nodes) ? workflow.nodes : WECHAT_STAGES,
      edges: Array.isArray(workflow?.edges) ? workflow.edges : WECHAT_STAGES.slice(0, -1).map((node, index) => ({ from: node.id, to: WECHAT_STAGES[index + 1].id })),
      updatedAt: workflow?.updatedAt || null,
    };
  }

  function safeWechatArtifactPath(relativePath) {
    const root = resolve(wechatArtifactDir);
    const candidate = resolve(root, String(relativePath || ""));
    if (!String(relativePath || "").trim() || (candidate !== root && !candidate.startsWith(`${root}${sep}`))) {
      throw Object.assign(new Error("公众号产物路径无效"), { statusCode: 409, code: "WECHAT_ARTIFACT_PATH_INVALID" });
    }
    return candidate;
  }

  function wechatArtifact(kind, absolutePath, mimeType) {
    const path = resolve(String(absolutePath || ""));
    const root = resolve(wechatArtifactDir);
    if (path !== root && !path.startsWith(`${root}${sep}`)) {
      throw Object.assign(new Error("公众号产物超出执行目录"), { code: "WECHAT_ARTIFACT_PATH_INVALID" });
    }
    return { kind, fileName: basename(path), relativePath: relative(root, path), mimeType };
  }

  async function runWechatInBackground(run) {
    if (!run?.id || activeWechatRuns.has(run.id)) return;
    activeWechatRuns.add(run.id);
    try {
      const latest = await wechatRuns.get(run.id, run.ownerUserId);
      if (!latest || !["QUEUED", "RUNNING"].includes(latest.status)) return;
      const result = await executeWechatRun({
        run: latest,
        artifactDir: wechatArtifactDir,
        planWithHermes: planWechatWithHermes,
        searchCompanyKnowledge: searchWechatKnowledge || (async ({ topic, prompt, limit = 8 }) => {
          const [documents, chunks, externalDocuments, owner] = await Promise.all([
            store.listKnowledgeDocuments({ status: "published" }),
            store.listKnowledgeChunks(),
            externalKnowledge.listDocuments().catch(() => []),
            store.getUserById(run.ownerUserId),
          ]);
          const scoped = filterKnowledgeScope([
            ...buildKnowledgeDocuments(documents.filter((document) => !document.externalized), chunks),
            ...externalDocuments,
          ], "公司通用知识", owner);
          return searchKnowledge(`${topic}\n${prompt}`.slice(0, 1_500), scoped, owner, { limit: Math.min(8, limit) })
            .map((hit) => ({ title: hit.title, excerpt: hit.excerpt, source: hit.source }));
        }),
        generateCover: generateWechatCover,
        onStage: async (event) => {
          await wechatRuns.appendEvent(run.id, event, run.ownerUserId);
        },
      });
      const artifacts = [
        wechatArtifact("preview", result.previewPath, "text/html; charset=utf-8"),
        wechatArtifact("body", join(dirname(result.previewPath), "公众号正文.html"), "text/html; charset=utf-8"),
        wechatArtifact("cover", result.coverPath, "image/png"),
        ...(Array.isArray(result.assetPaths) ? result.assetPaths.map((path) => wechatArtifact("image", path, "image/png")) : []),
      ];
      await wechatRuns.update(run.id, run.ownerUserId, { article: result.article, artifacts });
    } catch (error) {
      const latest = await wechatRuns.get(run.id, run.ownerUserId).catch(() => null);
      if (latest && !["FAILED", "BLOCKED", "REJECTED", "CANCELLED"].includes(latest.status)) {
        await wechatRuns.appendEvent(run.id, {
          stageId: latest.currentStageId || "input",
          status: "failed",
          error: cleanText(error?.message || "公众号工作流执行失败", 500),
          errorCode: cleanText(error?.code || "WECHAT_EXECUTION_FAILED", 160),
          completedAt: new Date().toISOString(),
        }, run.ownerUserId).catch(() => {});
      }
    } finally {
      activeWechatRuns.delete(run.id);
    }
  }
  const requireKnowledgeSyncAgent = async (req, _res, next) => {
    try {
      const supplied = String(req.get("x-knowledge-sync-secret") || "");
      const locallyAuthorized = Boolean(knowledgeSyncSecret) && secureEqualText(supplied, knowledgeSyncSecret);
      if (!locallyAuthorized && !await knowledgeSyncBridgeAuth(supplied)) {
        return next(new AuthError("同步入口不可用", "NOT_FOUND", 404));
      }
      next();
    } catch {
      next(new AuthError("同步入口不可用", "NOT_FOUND", 404));
    }
  };
  const requireOutreachReviewIngest = (req, _res, next) => {
    const supplied = String(req.get("x-outreach-review-secret") || "");
    if (!outreachReviewIngestSecret || !secureEqualText(supplied, outreachReviewIngestSecret)) {
      return next(new AuthError("审核入口不可用", "NOT_FOUND", 404));
    }
    next();
  };
  const requireLovartExecutor = (req, _res, next) => {
    const supplied = String(req.get("x-lovart-executor-secret") || "");
    if (!lovartRemoteEnabled || !secureEqualText(supplied, lovartExecutorSecret)) {
      return next(new AuthError("执行入口不可用", "NOT_FOUND", 404));
    }
    next();
  };
  const requireWeComContentBridge = (req, _res, next) => {
    const supplied = parseBearerToken(req.headers.authorization);
    if (!wecomContentEnabled || !secureEqualText(supplied, wecomCallbackConfig.bridgeToken)) {
      return next(new AuthError("执行入口不可用", "NOT_FOUND", 404));
    }
    next();
  };

  function knowhowErrorMessage(error) {
    if (error?.code === "KNOWHOW_MODEL_NOT_CONFIGURED") return "DeepSeek V4 Flash 尚未配置";
    if (error?.code === "KNOWHOW_PUBLIC_SOURCES_UNAVAILABLE") return "公开来源暂时不可用";
    if (error?.code === "KNOWHOW_SEARCH_FAILED") return "公开网页检索暂时不可用";
    if (error?.code === "KNOWHOW_SEARCH_EMPTY") return "联网搜索没有返回内容";
    if (error?.code === "KNOWHOW_SEARCH_INVALID_RESPONSE") return "联网搜索返回格式无效";
    if (error?.code === "KNOWHOW_SEARCH_NO_VERIFIED_SOURCES") return "未采集到可公开核验的近期原文";
    return "联网搜索暂时失败";
  }

  function knowhowItemKey(item) {
    const sourceUrl = String(item?.sourceUrl || "").trim().toLowerCase();
    const title = String(item?.title || "").normalize("NFKC").replace(/\s+/gu, "").toLowerCase();
    return sourceUrl ? `url:${sourceUrl}` : `title:${title}`;
  }

  async function archiveInvalidCollectorItems() {
    const candidates = (await store.listKnowhowItems())
      .filter((item) => item.status === "pending_review" && isInvalidCollectorItem(item));
    if (!candidates.length) return 0;
    const reviewedAt = new Date().toISOString();
    await Promise.all(candidates.map((item) => store.updateKnowhowItem(item.id, {
      status: "archived",
      reviewedAt,
      reviewReason: "系统归档：历史采集记录缺少可公开核验的原文链接、发布日期或有效摘要",
    })));
    return candidates.length;
  }

  async function collectAllKnowhowStreams({ actorUserId = null, trigger = "scheduler" } = {}) {
    const startedAt = new Date().toISOString();
    const legacyArchivedCount = await archiveInvalidCollectorItems();
    knowhowCollectorState = {
      ...knowhowCollectorState,
      running: true,
      lastStatus: "running",
      lastRunAt: startedAt,
      nextRunAt: new Date(Date.now() + knowhowCollectionIntervalMs).toISOString(),
    };
    const run = await store.createKnowhowRun({
      status: "running",
      trigger,
      streamCount: KNOWHOW_STREAMS.length,
      sourceCount: 0,
      modelId: DEFAULT_KNOWHOW_MODEL_ID,
      createdCount: 0,
      skippedCount: 0,
      failedCount: 0,
      legacyArchivedCount,
      startedAt,
      finishedAt: null,
      errors: [],
      outcomes: [],
    });
    const outcomes = [];
    const existingItems = await store.listKnowhowItems();
    const knownKeys = new Set(existingItems
      .filter((item) => Date.now() - Date.parse(item.createdAt || 0) <= 7 * 24 * 60 * 60 * 1000)
      .map((item) => knowhowItemKey(item)));
    let modelId = DEFAULT_KNOWHOW_MODEL_ID;
    for (const stream of KNOWHOW_STREAMS) {
      const streamOutcome = { streamId: stream.id, label: stream.label, status: "completed", createdCount: 0, skippedCount: 0, failedCount: 0, itemIds: [] };
      try {
        const searched = await knowhowModel({ stream, now: startedAt });
        modelId = searched?.modelId || modelId;
        const candidates = parseKnowhowCandidates({ content: searched?.content, stream, now: startedAt });
        if (!candidates.length) {
          throw Object.assign(new Error("没有可公开核验的近期原文"), { code: "KNOWHOW_SEARCH_NO_VERIFIED_SOURCES" });
        }
        for (const candidate of candidates) {
          const safeSourceUrl = candidate.sourceUrl ? validateKnowhowFetchUrl(candidate.sourceUrl).toString() : "";
          const safeCandidate = {
            ...candidate,
            title: redactSensitive(candidate.title),
            summary: redactSensitive(candidate.summary),
            sourceUrl: safeSourceUrl,
            category: redactSensitive(candidate.category || stream.category),
            vertical: redactSensitive(candidate.vertical || stream.vertical),
            tags: (Array.isArray(candidate.tags) ? candidate.tags : []).map((tag) => redactSensitive(tag)),
          };
          const key = knowhowItemKey(safeCandidate);
          if (knownKeys.has(key)) {
            streamOutcome.skippedCount += 1;
            continue;
          }
          const item = await store.createKnowhowItem({
            ...safeCandidate,
            ownerUserId: null,
            ownerName: "采集 Worker",
            submittedBy: "collector_worker",
            collectionRunId: run.id,
          });
          knownKeys.add(key);
          streamOutcome.createdCount += 1;
          streamOutcome.itemIds.push(item.id);
        }
      } catch (error) {
        streamOutcome.status = "failed";
        streamOutcome.failedCount = 1;
        streamOutcome.error = knowhowErrorMessage(error);
      }
      outcomes.push(streamOutcome);
    }
    const createdCount = outcomes.reduce((total, item) => total + item.createdCount, 0);
    const skippedCount = outcomes.reduce((total, item) => total + item.skippedCount, 0);
    const failedCount = outcomes.reduce((total, item) => total + item.failedCount, 0);
    const errors = outcomes.filter((item) => item.error).map((item) => `${item.label}：${item.error}`);
    const finishedAt = new Date().toISOString();
    const updatedRun = await store.updateKnowhowRun(run.id, {
      status: failedCount && (createdCount || skippedCount) ? "partial" : failedCount ? "failed" : "completed",
      modelId,
      createdCount,
      skippedCount,
      failedCount,
      legacyArchivedCount,
      finishedAt,
      outcomes,
      errors,
    });
    knowhowCollectorState = {
      ...knowhowCollectorState,
      running: false,
      modelId,
      lastRunAt: finishedAt,
      nextRunAt: new Date(Date.now() + knowhowCollectionIntervalMs).toISOString(),
      lastStatus: updatedRun.status,
      lastError: errors[0] || null,
    };
    if (actorUserId) {
      await store.addAudit({
        action: "workspace.knowhow_collection_run",
        actorUserId,
        targetType: "knowhow_run",
        targetId: run.id,
        metadata: { trigger, streamCount: KNOWHOW_STREAMS.length, createdCount, skippedCount, failedCount, legacyArchivedCount, modelId },
      });
    }
    return updatedRun;
  }

  async function runKnowhowCollection(optionsForRun = {}) {
    if (knowhowCollectionPromise) return knowhowCollectionPromise;
    knowhowCollectionPromise = collectAllKnowhowStreams(optionsForRun).finally(() => {
      knowhowCollectionPromise = null;
    });
    return knowhowCollectionPromise;
  }

  async function syncKnowhowKnowledgeDocument(item, actorUserId) {
    const existing = (await store.listKnowledgeDocuments({ knowhowItemId: item.id }))[0] || null;
    if (item.status !== "published") {
      if (!existing) return null;
      return store.updateKnowledgeDocument(existing.id, {
        status: "archived",
        workflowStatus: "archived",
        archivedAt: new Date().toISOString(),
        archivedBy: actorUserId || null,
      });
    }

    const timestamp = new Date().toISOString();
    const content = redactSensitive([
      item.summary,
      item.category ? `分类：${item.category}` : "",
      item.vertical ? `行业：${item.vertical}` : "",
      Array.isArray(item.tags) && item.tags.length ? `标签：${item.tags.join("、")}` : "",
      item.sourceUrl ? `来源：${item.sourceUrl}` : "",
    ].filter(Boolean).join("\n"));
    const patch = {
      title: item.title,
      ownerUserId: item.ownerUserId || actorUserId || null,
      ownerId: item.ownerUserId || actorUserId || null,
      visibility: item.visibility || "company",
      scope: item.visibility || "company",
      space: "公司通用知识",
      spaces: ["公司通用知识", item.category].filter(Boolean),
      status: "published",
      workflowStatus: "index_completed",
      knowledgeType: "knowhow",
      aiSummary: cleanText(item.summary, 360),
      sensitive: false,
      requiresAdminReview: false,
      source: "Know-how采集看板",
      sourcePath: item.sourceUrl || "Know-how看板",
      sourceUpdatedAt: item.updatedAt || timestamp,
      externalized: false,
      knowhowItemId: item.id,
      publishedAt: item.publishedAt || timestamp,
      workflowHistory: [
        ...(Array.isArray(existing?.workflowHistory) ? existing.workflowHistory : []),
        { status: "index_completed", at: timestamp, actorUserId: actorUserId || null, source: "knowhow_board" },
      ],
    };
    if (existing) return store.updateKnowledgeDocument(existing.id, patch);

    const document = await store.createKnowledgeDocument({
      fileId: null,
      ...patch,
      createdFrom: "knowhow_board",
    });
    await store.addKnowledgeChunks(document.id, [{ content, position: 0 }]);
    return document;
  }

  async function resolveStoredUpload(file) {
    if (file?.storageBackend === "oss") {
      throw Object.assign(new Error("OSS 文件需要通过签名地址读取"), { statusCode: 500, code: "OSS_SIGNED_URL_REQUIRED" });
    }
    const resolvedPath = resolve(file?.storagePath || "");
    if (!resolvedPath.startsWith(`${dataDir}${sep}`)) {
      throw Object.assign(new Error("文件存储位置无效"), { statusCode: 500, code: "INVALID_STORAGE_PATH" });
    }
    const canonicalPath = await realpath(resolvedPath);
    const canonicalDataDir = await realpath(dataDir);
    if (!canonicalPath.startsWith(`${canonicalDataDir}${sep}`)) {
      throw Object.assign(new Error("文件存储位置无效"), { statusCode: 500, code: "INVALID_STORAGE_PATH" });
    }
    return canonicalPath;
  }

  async function persistWorkspaceFile({ fileId, ownerUserId, originalName, sourcePath, mimeType, sha256, createdAt }) {
    if (!workspaceObjectStorageEnabled) {
      return { storageBackend: "server_disk", storagePath: sourcePath };
    }
    if (!workspaceObjectStore.isConfigured()) {
      throw Object.assign(new Error("工作台私有 OSS 尚未配置"), { statusCode: 503, code: "WORKSPACE_OSS_NOT_CONFIGURED" });
    }
    const storageKey = buildWorkspaceObjectKey({
      prefix: workspaceObjectStore.config.prefix,
      ownerUserId,
      fileId,
      originalName,
      createdAt,
    });
    const uploaded = await workspaceObjectStore.putFile({ key: storageKey, path: sourcePath, contentType: mimeType, sha256 });
    return {
      storageBackend: "oss",
      storageKey,
      storageEtag: uploaded.etag || null,
      storagePath: null,
    };
  }

  const imageReferenceCacheTtlMs = Math.max(60_000, Number(
    options.imageReferenceCacheTtlMs
      ?? process.env.IMAGE_REFERENCE_CACHE_TTL_MS
      ?? 24 * 60 * 60 * 1000,
  ) || 24 * 60 * 60 * 1000);

  async function releaseImageReferenceCache(fileIds, reason = "expired") {
    for (const fileId of [...new Set(Array.isArray(fileIds) ? fileIds : [])]) {
      const file = await store.getFile(fileId).catch(() => null);
      if (!file || file.temporaryCache !== true || file.status === "deleted") continue;
      const cachedPath = resolve(file.storagePath || "");
      if (file.storagePath && cachedPath.startsWith(`${dataDir}${sep}`)) {
        await rm(cachedPath, { force: true }).catch(() => {});
      }
      await store.updateFile(file.id, {
        status: "deleted",
        storagePath: null,
        cacheDeletedAt: new Date().toISOString(),
        cacheDeleteReason: reason,
      });
    }
  }

  async function purgeExpiredImageReferenceCache() {
    const now = Date.now();
    const expiredIds = (await store.listFiles())
      .filter((file) => file.temporaryCache === true
        && file.status !== "deleted"
        && Number.isFinite(Date.parse(file.cacheExpiresAt || ""))
        && Date.parse(file.cacheExpiresAt) <= now)
      .map((file) => file.id);
    await releaseImageReferenceCache(expiredIds, "expired");
  }

  for (const job of interruptedImageJobs) {
    await releaseImageReferenceCache(job.attachmentIds, "job_interrupted");
  }

  async function finalizeImageJob(job, generated, attachmentCount = 0) {
    const storedImages = [];
    for (const [index, artifact] of generated.artifacts.entries()) {
      const branded = await brandGeneratedImageImpl({
        imagePath: artifact.path,
        logoPath: imageBrandLogoPath,
      });
      const buffer = await readFile(branded.path);
      const originalName = `创创作图_${job.modelLabel}_${String(index + 1).padStart(2, "0")}${branded.extension}`;
      const fileId = randomUUID();
      const fileCreatedAt = new Date().toISOString();
      const sha256 = createHash("sha256").update(buffer).digest("hex");
      const persistentStorage = await persistWorkspaceFile({
        fileId,
        ownerUserId: job.ownerUserId,
        originalName,
        sourcePath: branded.path,
        mimeType: branded.mimeType,
        sha256,
        createdAt: fileCreatedAt,
      });
      const storedFile = await store.createFile({
        id: fileId,
        createdAt: fileCreatedAt,
        ownerUserId: job.ownerUserId,
        departmentId: job.departmentId || null,
        originalName,
        ...persistentStorage,
        mimeType: branded.mimeType,
        size: branded.size,
        sha256,
        scope: "personal",
        space: "视觉作图",
        status: "ready",
        malwareScanStatus: "not_required",
        submitAsCompanyKnowledge: false,
        workflowStatus: (generated.provider || imageProvider) === "lovart" ? "lovart_generated_image" : "libimage_generated_image",
        artifactType: "generated_image",
        imageJobId: job.id,
        imageModel: job.model,
        imageModelLabel: job.modelLabel,
        imageProvider: generated.provider || imageProvider,
        imageBranded: branded.branded === true,
        imageBrandPlacement: branded.placement || null,
        lovartThreadId: generated.threadId || generated.taskId || null,
      });
      storedImages.push(storedFile);
      if (persistentStorage.storageBackend === "oss") await rm(branded.path, { force: true });
    }
    const artifacts = storedImages.map(serializeGeneratedImage);
    const message = await store.addMessage({
      conversationId: job.conversationId,
      ownerUserId: job.ownerUserId,
      role: "assistant",
      content: `图片已生成，共 ${artifacts.length} 张。`,
      route: (generated.provider || imageProvider) === "lovart" ? "lovart" : "libimage",
      module: "视觉作图",
      artifacts,
      imageModel: job.model,
      imageModelLabel: job.modelLabel,
      imageMode: job.mode || generated.mode || "text-to-image",
      imageProvider: generated.provider || imageProvider,
      creditCost: imageCreditCost,
    });
    const completedAt = new Date().toISOString();
    await store.updateImageJob(job.id, {
      status: "done",
      artifactIds: storedImages.map((file) => file.id),
      messageId: message.id,
      provider: generated.provider || imageProvider,
      providerTaskId: generated.taskId || generated.threadId || null,
      lovartThreadId: generated.threadId || generated.taskId || null,
      completedAt,
    });
    await store.updateConversation(job.conversationId, {
      lastMessageAt: message.createdAt,
      module: "视觉作图",
    });
    await store.addAudit({
      action: "workspace.image_generated",
      actorUserId: job.ownerUserId,
      targetType: "image_job",
      targetId: job.id,
      metadata: {
        model: job.model,
        mode: job.mode || "text-to-image",
        provider: generated.provider || imageProvider,
        creditCost: imageCreditCost,
        artifactCount: storedImages.length,
        attachmentCount,
        executor: lovartRemoteEnabled ? "manager-office" : "local",
      },
    });
    return storedImages;
  }

  async function createKnowledgeCardArtifacts({ ownerUserId, departmentId, content }) {
    const svg = renderKnowledgeCardSvg({ content });
    const buffer = Buffer.from(svg, "utf8");
    const artifactKey = randomUUID();
    const svgPath = join(artifactDir, `knowledge-card-${artifactKey}.svg`);
    await writeFile(svgPath, buffer, { mode: 0o600 });
    const svgFile = await store.createFile({
      ownerUserId,
      departmentId: departmentId || null,
      originalName: `创创知识卡_${new Date().toISOString().slice(0, 10)}.svg`,
      storagePath: svgPath,
      mimeType: "image/svg+xml",
      size: buffer.length,
      sha256: createHash("sha256").update(buffer).digest("hex"),
      scope: "personal",
      space: "知识卡",
      status: "ready",
      malwareScanStatus: "not_required",
      submitAsCompanyKnowledge: false,
      workflowStatus: "knowledge_card_source_generated",
      artifactType: "knowledge_card_source",
      imageModel: "code-renderer",
      imageModelLabel: "知识卡 SVG 源文件",
      imageProvider: "workspace-dom",
    });
    const png = renderKnowledgeCardPng({ content });
    const pngPath = join(artifactDir, `knowledge-card-${artifactKey}.png`);
    await writeFile(pngPath, png, { mode: 0o600 });
    const pngFile = await store.createFile({
      ownerUserId,
      departmentId: departmentId || null,
      originalName: `创创知识卡_${new Date().toISOString().slice(0, 10)}.png`,
      storagePath: pngPath,
      mimeType: "image/png",
      size: png.length,
      sha256: createHash("sha256").update(png).digest("hex"),
      scope: "personal",
      space: "知识卡",
      status: "ready",
      malwareScanStatus: "not_required",
      submitAsCompanyKnowledge: false,
      workflowStatus: "knowledge_card_generated",
      artifactType: "knowledge_card_image",
      imageModel: "code-renderer",
      imageModelLabel: "知识卡 PNG 代码渲染",
      imageProvider: "workspace-dom",
    });
    return { pngFile, svgFile };
  }

  async function processImageJob(jobId, referenceFiles) {
    const outputDir = join(artifactDir, "image-jobs", jobId);
    let reservation;
    let completed = false;
    try {
      const job = await store.getImageJob(jobId);
      if (!job) throw Object.assign(new Error("图片任务不存在"), { code: "IMAGE_JOB_NOT_FOUND" });
      await store.updateImageJob(jobId, { status: "running", startedAt: new Date().toISOString() });
      reservation = await reserveStorageCapacity(job.ownerUserId, 50 * 1024 * 1024);
      const attachmentPaths = [];
      for (const file of referenceFiles) attachmentPaths.push(await resolveStoredUpload(file));
      const generated = await imageClient.generateImage({
        prompt: job.prompt,
        model: job.model,
        mode: job.mode || (attachmentPaths.length ? "image-to-image" : "text-to-image"),
        attachmentPaths,
        outputDir,
      });
      await releaseImageReferenceCache(job.attachmentIds, "job_completed");
      await finalizeImageJob(job, generated, referenceFiles.length);
      completed = true;
    } catch (error) {
      const safe = safeError(error);
      const refunded = await store.failImageJobAndRefund(jobId, {
        errorCode: error?.code || "IMAGE_GENERATION_FAILED",
        errorMessage: safe.message,
      }).catch(() => null);
      await store.addAudit({
        action: "workspace.image_generation_failed",
        actorUserId: refunded?.job?.ownerUserId || null,
        targetType: "image_job",
        targetId: jobId,
        metadata: { code: error?.code || "IMAGE_GENERATION_FAILED", creditRefunded: true },
      }).catch(() => {});
    } finally {
      const job = await store.getImageJob(jobId).catch(() => null);
      if (job?.ownerUserId) activeImageUsers.delete(job.ownerUserId);
      activeImageJobs.delete(jobId);
      await releaseStorageReservation(reservation).catch(() => {});
      await releaseImageReferenceCache(job?.attachmentIds, completed ? "job_completed" : "job_failed").catch(() => {});
      if (!completed) await rm(outputDir, { recursive: true, force: true }).catch(() => {});
    }
  }

  async function maintainRemoteImageJobs() {
    if (!lovartRemoteEnabled) return;
    const now = Date.now();
    const jobs = await store.listImageJobs({ status: ["queued", "running"] });
    for (const job of jobs) {
      const referenceTime = Date.parse(job.claimedAt || job.startedAt || job.createdAt || job.updatedAt || "");
      if (!Number.isFinite(referenceTime) || now - referenceTime < imageRemoteJobTimeoutMs) continue;
      await releaseImageReferenceCache(job.attachmentIds, "job_timeout").catch(() => {});
      const failed = await store.failImageJobAndRefund(job.id, {
        errorCode: "IMAGE_EXECUTOR_TIMEOUT",
        errorMessage: "图片生成执行器响应超时，积分已自动退回，请稍后重试",
      }).catch(() => null);
      if (!failed?.job || failed.job.status !== "failed") continue;
      activeImageUsers.delete(failed.job.ownerUserId);
      activeImageJobs.delete(failed.job.id);
      await store.addAudit({
        action: "workspace.image_generation_failed",
        actorUserId: failed.job.ownerUserId,
        targetType: "image_job",
        targetId: failed.job.id,
        metadata: {
          code: "IMAGE_EXECUTOR_TIMEOUT",
          creditRefunded: failed.job.creditRefunded === true,
          executor: "manager-office",
        },
      }).catch(() => {});
      logDiagnostic("warn", "image_executor_timeout", {
        imageJobId: failed.job.id,
        model: failed.job.model,
        statusBeforeTimeout: job.status,
        ageMs: now - referenceTime,
      });
    }
  }

  async function syncBundle(documentId, { includeChunks = false } = {}) {
    const document = await store.getKnowledgeDocument(documentId);
    if (!document || document.status !== "approved"
      || !["nas_sync_pending", "nas_sync_failed"].includes(document.workflowStatus)
      || document.customerDataDetected
      || document.dataClassification === "restricted_customer_data") {
      throw Object.assign(new Error("待同步资料不存在"), { statusCode: 404, code: "SYNC_ITEM_NOT_FOUND" });
    }
    const file = document.fileId ? await store.getFile(document.fileId) : null;
    if (!file || file.status !== "approved" || !["clean", "not_required"].includes(file.malwareScanStatus)) {
      throw Object.assign(new Error("资料尚未通过文件安全检查"), { statusCode: 423, code: "SYNC_FILE_NOT_READY" });
    }
    let chunks = includeChunks ? await store.listKnowledgeChunks(document.id) : [];
    if (includeChunks && !chunks.length) {
      const canonicalPath = await resolveStoredUpload(file);
      const buffer = await readFile(canonicalPath);
      const extracted = await extractAndChunkDocument(
        { name: file.originalName, mimeType: file.mimeType, buffer },
        { idPrefix: file.id },
      );
      if (!extracted.text || containsRestrictedBusinessContent(extracted.text)) {
        throw Object.assign(new Error("资料不符合员工知识范围"), { statusCode: 403, code: "KNOWLEDGE_SCOPE_DENIED" });
      }
      chunks = await store.addKnowledgeChunks(document.id, extracted.chunks.map((chunk) => ({
        content: redactSensitive(chunk.content),
        position: chunk.index,
      })));
    }
    return {
      document,
      file,
      item: {
        id: document.id,
        fileId: file.id,
        title: document.title,
        originalName: file.originalName,
        mimeType: file.mimeType,
        size: Number(file.size || 0),
        sha256: file.sha256,
        knowledgeType: document.knowledgeType || file.knowledgeType || "其他",
        aiSummary: document.aiSummary || "",
        visibility: document.visibility || "company",
        departmentId: document.departmentId || null,
        sensitive: document.sensitive === true,
        sensitiveCategories: Array.isArray(document.sensitiveCategories) ? document.sensitiveCategories : [],
        reviewedAt: document.reviewedAt || null,
        targetDirectory: document.targetDirectory || `公司知识/${document.knowledgeType || "其他"}`,
        chunks: includeChunks ? chunks.map((chunk) => ({
          id: chunk.id,
          index: Number(chunk.position || 0),
          content: redactSensitive(cleanText(chunk.content, 12_000)),
        })) : undefined,
      },
    };
  }

  const geoActor = (req) => req.workspace.user;
  const geoFilter = (req) => Object.fromEntries(Object.entries({
    projectId: req.query.projectId,
    runId: req.query.runId,
    status: req.query.status,
  }).filter(([, value]) => value !== undefined && value !== ""));

  app.get("/workspace-api/geo/overview", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, overview: await geoService.overview(geoActor(req), req.query) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/projects", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, projects: await geoService.listProjects(geoActor(req)) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/runs", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, runs: await geoService.listRuns(geoActor(req), geoFilter(req)) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/workflow", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, workflow: await geoService.workflow(geoActor(req), req.query) }); } catch (error) { next(error); }
  });
  app.patch("/workspace-api/geo/workflow/mode", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const config = await geoService.updateWorkflowMode(geoActor(req), { ...req.body, idempotencyKey: req.get("idempotency-key") });
      res.json({ ok: true, config });
    } catch (error) { next(error); }
  });
  app.post("/workspace-api/geo/runs", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const run = await geoService.startRun(geoActor(req), { ...req.body, idempotencyKey: req.get("idempotency-key") });
      res.status(run.replayed ? 200 : 201).json({ ok: true, run });
    } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/runs/:id", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, run: await geoService.runDetails(geoActor(req), req.params.id) }); } catch (error) { next(error); }
  });
  app.post("/workspace-api/geo/observations/import", requireUser, mutationLimiter, async (req, res, next) => {
    try { res.status(201).json({ ok: true, observation: await geoService.importObservation(geoActor(req), req.body) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/opportunities", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, opportunities: await geoService.listOpportunities(geoActor(req), geoFilter(req)) }); } catch (error) { next(error); }
  });
  app.post("/workspace-api/geo/opportunities/:id/content", requireUser, mutationLimiter, async (req, res, next) => {
    try { res.status(201).json({ ok: true, assets: await geoService.generateContent(geoActor(req), req.params.id) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/content-assets", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, assets: await geoService.listContentAssets(geoActor(req), geoFilter(req)) }); } catch (error) { next(error); }
  });
  app.patch("/workspace-api/geo/content-assets/:id", requireUser, mutationLimiter, async (req, res, next) => {
    try { res.json({ ok: true, asset: await geoService.editContentAsset(geoActor(req), req.params.id, req.body) }); } catch (error) { next(error); }
  });
  app.post("/workspace-api/geo/content-assets/:id/submit-review", requireUser, mutationLimiter, async (req, res, next) => {
    try { res.json({ ok: true, asset: await geoService.submitReview(geoActor(req), req.params.id) }); } catch (error) { next(error); }
  });
  app.post("/workspace-api/geo/content-assets/:id/review", requireUser, requireKnowledgeReviewer, mutationLimiter, async (req, res, next) => {
    try { res.json({ ok: true, asset: await geoService.reviewAsset(geoActor(req), req.params.id, req.body) }); } catch (error) { next(error); }
  });
  app.post("/workspace-api/geo/content-assets/:id/publications", requireUser, requireKnowledgeReviewer, mutationLimiter, async (req, res, next) => {
    try { res.status(201).json({ ok: true, publication: await geoService.registerPublication(geoActor(req), req.params.id, req.body) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/retests", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, retests: await geoService.listRetests(geoActor(req), geoFilter(req)) }); } catch (error) { next(error); }
  });
  app.post("/workspace-api/geo/retests/:id/run", requireUser, mutationLimiter, async (req, res, next) => {
    try { res.json({ ok: true, retest: await geoService.runRetest(geoActor(req), req.params.id) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/connectors/status", requireUser, async (req, res, next) => {
    try { res.json({ ok: true, ...(await geoService.connectors(geoActor(req))) }); } catch (error) { next(error); }
  });
  app.get("/workspace-api/geo/audit", requireUser, requireAdmin, async (req, res, next) => {
    try { res.json({ ok: true, events: await geoService.audit(geoActor(req)) }); } catch (error) { next(error); }
  });

  app.get("/workspace-api/healthz", async (_req, res) => {
    const coursewareRuntime = await getCoursewareRuntimeStatus();
    const documentRuntime = await getDocumentRuntimeStatus();
    const checks = {
      storage: { status: "ready" },
      disk: { status: "ready" },
      courseware: coursewareRuntime,
      document: documentRuntime,
      externalKnowledge: { status: externalKnowledge.enabled ? "configured" : "not_configured" },
      crmReadonly: { status: crmReadonly.enabled ? "configured" : "not_configured" },
      agentHarness: {
        status: agentHarness.enabled ? "configured" : "not_configured",
        runtime: agentHarness.runtime || "hermes",
        required: agentHarness.required === true,
        executionIdentity: Boolean(agentHarness.ownerKey && agentHarness.runnerKey) ? "configured" : "invalid",
      },
    };
    try {
      const storage = typeof store.healthCheck === "function"
        ? await store.healthCheck()
        : { readable: Boolean(await store.snapshot()) };
      checks.storage = {
        status: storage?.readable === false
          ? "unavailable"
          : Number(storage?.stateBytes || 0) >= Number(storage?.warningStateBytes || Number.POSITIVE_INFINITY)
            ? "capacity_warning"
            : "ready",
        schemaVersion: Number(storage?.schemaVersion || 0) || undefined,
        stateBytes: Number(storage?.stateBytes || 0) || undefined,
        warningStateBytes: Number(storage?.warningStateBytes || 0) || undefined,
        maximumStateBytes: Number(storage?.maximumStateBytes || 0) || undefined,
      };
    } catch {
      checks.storage = { status: "unavailable" };
    }
    const minimumFreeBytes = numberFromEnv("MIN_FREE_STORAGE_MB", 1024) * 1024 * 1024;
    try {
      const disk = await statfsImpl(dataDir);
      const freeBytes = Number(disk.bavail) * Number(disk.bsize);
      checks.disk = {
        status: Number.isFinite(freeBytes) && freeBytes >= minimumFreeBytes ? "ready" : "low",
        freeBytes: Number.isFinite(freeBytes) ? freeBytes : null,
        minimumFreeBytes,
      };
    } catch {
      checks.disk = { status: "unavailable", minimumFreeBytes };
    }
    const coreReady = checks.storage.status !== "unavailable"
      && checks.courseware.status === "ready"
      && !lifecycleState.shuttingDown;
    const degraded = checks.storage.status !== "ready"
      || !coreReady
      || checks.disk.status !== "ready"
      || checks.courseware.status !== "ready"
      || checks.document.status !== "ready"
      || checks.externalKnowledge.status !== "configured"
      || checks.crmReadonly.status !== "configured"
      || (checks.agentHarness.required && checks.agentHarness.status !== "configured")
      || checks.agentHarness.executionIdentity === "invalid";
    res.status(coreReady ? 200 : 503).json({
      ok: coreReady,
      status: degraded ? "degraded" : "ready",
      checks,
      timestamp: new Date().toISOString(),
    });
  });

  app.get("/workspace-api/release-center/healthz", requireUser, async (_req, res) => {
    const manifestHealth = await releaseCenterManifestHealth(RELEASE_CENTER_MANIFEST_PATH);
    const health = manifestHealth.ok || manifestHealth.code !== "RELEASE_CENTER_MANIFEST_UNAVAILABLE"
      ? manifestHealth
      : await releaseCenterHealth(RELEASE_CENTER_LOCAL_ROOT);
    res.status(health.ok ? 200 : 503).json(health);
  });

  app.get("/workspace-api/release-center/list", requireUser, async (req, res, next) => {
    try {
      let listing;
      const manifestHealth = await releaseCenterManifestHealth(RELEASE_CENTER_MANIFEST_PATH);
      if (manifestHealth.ok) {
        listing = await listManifestDirectory(RELEASE_CENTER_MANIFEST_PATH, req.query.path || "");
      } else if (manifestHealth.code === "RELEASE_CENTER_MANIFEST_UNAVAILABLE") {
        listing = await listReleaseDirectory(RELEASE_CENTER_LOCAL_ROOT, req.query.path || "");
      } else {
        throw Object.assign(new Error("发布中心索引已过期，请稍后重试"), { statusCode: 503, code: manifestHealth.code });
      }
      res.json({ ok: true, ...listing });
    } catch (error) {
      next(error);
    }
  });

  app.get("/workspace-api/release-center/file", requireUser, async (req, res, next) => {
    try {
      const manifestHealth = await releaseCenterManifestHealth(RELEASE_CENTER_MANIFEST_PATH);
      if (manifestHealth.ok) {
        const item = await getManifestFile(RELEASE_CENTER_MANIFEST_PATH, req.query.path || "");
        if (item.downloadUrl) return res.redirect(302, item.downloadUrl);
      } else if (manifestHealth.code !== "RELEASE_CENTER_MANIFEST_UNAVAILABLE") {
        throw Object.assign(new Error("发布中心索引已过期，请稍后重试"), { statusCode: 503, code: manifestHealth.code });
      }
      const filePath = resolveReleasePath(RELEASE_CENTER_LOCAL_ROOT, req.query.path || "");
      const fileStat = await stat(filePath);
      if (!fileStat.isFile()) {
        throw Object.assign(new Error("目标不是文件"), { statusCode: 400, code: "RELEASE_FILE_REQUIRED" });
      }
      res.download(filePath, basename(filePath), { dotfiles: "deny", cacheControl: false }, (error) => {
        if (error && !res.headersSent) next(error);
      });
    } catch (error) {
      next(error);
    }
  });

  app.get("/workspace-api/wecom/content/callback", (req, res) => {
    if (!wecomContentEnabled || !wecomCallbackCrypto) return res.status(404).type("text/plain").send("not found");
    try {
      const echo = wecomCallbackCrypto.verifyUrl({
        echoStr: cleanText(req.query?.echostr, 8_192),
        timestamp: cleanText(req.query?.timestamp, 40),
        nonce: cleanText(req.query?.nonce, 128),
        messageSignature: cleanText(req.query?.msg_signature, 128),
      });
      return res.status(200).type("text/plain").send(echo);
    } catch {
      return res.status(403).type("text/plain").send("invalid signature");
    }
  });

  app.post("/workspace-api/wecom/content/callback", async (req, res) => {
    if (!wecomContentEnabled || !wecomCallbackCrypto || !wecomContentTasks) return res.status(404).type("text/plain").send("not found");
    try {
      const { message } = wecomCallbackCrypto.parseCallback({
        xml: typeof req.body === "string" ? req.body : "",
        timestamp: cleanText(req.query?.timestamp, 40),
        nonce: cleanText(req.query?.nonce, 128),
        messageSignature: cleanText(req.query?.msg_signature, 128),
      });
      if (!message.fromUserName) return res.status(200).type("text/plain").send("success");
      const normalizedContent = cleanText(message.content, 2_000);
      const isHelp = message.msgType === "text" && /^(?:帮助|菜单|help)$/iu.test(normalizedContent);
      if (message.msgType !== "text" || !normalizedContent || isHelp) {
        res.status(200).type("text/plain").send("success");
        if (typeof wecomClient.sendText === "function") {
          const reply = isHelp
            ? "创创 AI 总调度官：请直接发一句选题或制作要求。我会自动召集所需的数字员工，完成后把作品或今日内参发回这里。"
            : "创创 AI 总调度官：目前请发送文字选题或制作要求，我会据此自动组建内容工作流。";
          queueMicrotask(() => { void wecomClient.sendText(message.fromUserName, reply).catch(() => {}); });
        }
        return;
      }
      const messageId = message.msgId || createHash("sha256")
        .update(`${message.fromUserName}:${message.createTime}:${normalizedContent}`, "utf8")
        .digest("hex");
      const queued = await wecomContentTasks.enqueue({
        messageId,
        userId: message.fromUserName,
        content: normalizedContent,
      });
      res.status(200).type("text/plain").send("success");
      if (!queued.duplicate && typeof wecomClient.sendText === "function") {
        queueMicrotask(() => {
          void wecomClient.sendText(
            message.fromUserName,
            `创创 AI 总调度官：收到“${normalizedContent.slice(0, 80)}”。我现在召集数字员工、编排真实工作流；作品或内参完成后直接发回本对话。`,
          ).catch((error) => logDiagnostic("warn", "wecom_content_ack_failed", { code: safeDiagnosticCode(error?.code, "WECOM_SEND_FAILED") }));
        });
      }
    } catch {
      res.status(403).type("text/plain").send("invalid message");
    }
  });

  app.get("/workspace-api/wecom/content/status", (_req, res) => {
    res.json({ ok: true, enabled: wecomContentEnabled });
  });

  app.post("/workspace-api/wecom/content/lease", requireWeComContentBridge, async (req, res, next) => {
    try {
      const task = await wecomContentTasks.leaseNext({ workerId: cleanText(req.body?.workerId, 80) || "boss-office" });
      res.json({ ok: true, task });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/wecom/content/tasks/:id/progress", requireWeComContentBridge, async (req, res, next) => {
    try {
      const task = await wecomContentTasks.update(cleanText(req.params.id, 80), cleanText(req.body?.leaseId, 80), {
        status: cleanText(req.body?.status, 40) || "running",
        progress: req.body?.progress,
        stage: cleanText(req.body?.stage, 160),
        runId: cleanText(req.body?.runId, 160),
      });
      if (!task) throw Object.assign(new Error("任务租约已失效"), { statusCode: 409, code: "WECOM_TASK_LEASE_EXPIRED" });
      if (req.body?.notify === true && typeof wecomClient.sendText === "function") {
        await wecomClient.sendText(task.userId, `创创 AI 总调度官：${task.stage}（${task.progress}%）`);
      }
      res.json({ ok: true, task });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/wecom/content/tasks/:id/supersede", requireWeComContentBridge, async (req, res, next) => {
    try {
      const task = await wecomContentTasks.supersede(
        cleanText(req.params.id, 80),
        cleanText(req.body?.leaseId, 80),
        {
          replacementTaskId: cleanText(req.body?.replacementTaskId, 80),
          reason: cleanText(req.body?.reason, 240) || "最新企业微信指令优先",
        },
      );
      if (!task) throw Object.assign(new Error("任务租约已失效"), { statusCode: 409, code: "WECOM_TASK_LEASE_EXPIRED" });
      res.json({ ok: true, task });
      if (typeof wecomClient.sendText === "function") {
        queueMicrotask(() => {
          void wecomClient.sendText(task.userId, "创创 AI 总调度官：收到更新指令，上一任务已终止并退出队列，正在执行最新任务。").catch(() => {});
        });
      }
    } catch (error) { next(error); }
  });

  app.post(
    "/workspace-api/wecom/content/tasks/:id/complete",
    requireWeComContentBridge,
    wecomContentUpload.fields([
      { name: "video", maxCount: 1 },
      { name: "images", maxCount: 6 },
    ]),
    async (req, res, next) => {
      let task;
      const videoFiles = Array.isArray(req.files?.video) ? req.files.video : [];
      const imageFiles = Array.isArray(req.files?.images) ? req.files.images : [];
      const uploadedFiles = [...videoFiles, ...imageFiles];
      try {
        if ((videoFiles.length === 1 && imageFiles.length > 0) || (videoFiles.length === 0 && imageFiles.length === 0)) {
          throw Object.assign(new Error("请上传单个 MP4 成片或 1 至 6 张 PNG 图片"), { statusCode: 400, code: "WECOM_ARTIFACT_REQUIRED" });
        }
        const type = videoFiles.length === 1 ? "video" : "images";
        const pageCountField = Number.parseInt(String(req.body?.pageCount || ""), 10);
        const sourceCountField = Number.parseInt(String(req.body?.sourceCount || ""), 10);
        const pageCount = type === "images"
          ? (Number.isInteger(pageCountField) && pageCountField >= 1 && pageCountField <= 6 ? pageCountField : imageFiles.length)
          : null;
        const sourceCount = Number.isInteger(sourceCountField) && sourceCountField >= 0
          ? Math.min(sourceCountField, 100_000)
          : null;
        task = await wecomContentTasks.update(cleanText(req.params.id, 80), cleanText(req.body?.leaseId, 80), {
          status: "delivering",
          progress: 99,
          stage: type === "video" ? "成片完成，正在发送到企业微信" : "今日内参完成，正在发送到企业微信",
          runId: cleanText(req.body?.runId, 160),
        });
        if (!task) throw Object.assign(new Error("任务租约已失效"), { statusCode: 409, code: "WECOM_TASK_LEASE_EXPIRED" });
        const title = cleanText(req.body?.title, 128)
          || (type === "images" ? "创创·服装生意内参" : cleanText(task.content, 128))
          || "创创 AI 生成作品";
        if (typeof wecomClient.uploadMedia !== "function"
          || (type === "video" && typeof wecomClient.sendVideo !== "function")
          || (type === "images" && (typeof wecomClient.sendImage !== "function" || typeof wecomClient.sendText !== "function"))) {
          throw new WeComError("企业微信作品回传尚未启用", "WECOM_MEDIA_NOT_CONFIGURED", 503);
        }
        if (type === "video") {
          const video = videoFiles[0];
          const data = await readFile(video.path);
          const media = await wecomClient.uploadMedia({
            type: "video",
            filename: video.originalname || `创创AI作品-${task.id}.mp4`,
            data,
            mimeType: "video/mp4",
          });
          await wecomClient.sendVideo(task.userId, {
            mediaId: media.mediaId,
            title,
            description: "AI主管已完成调度，真实工作流生产完毕。点击即可播放。",
          });
        } else {
          const images = [];
          for (const image of imageFiles) {
            const data = await readFile(image.path);
            const pngSignature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
            if (data.length < pngSignature.length || !data.subarray(0, pngSignature.length).equals(pngSignature)) {
              throw Object.assign(new Error("内参图片不是有效的 PNG 文件"), { statusCode: 415, code: "WECOM_PNG_INVALID" });
            }
            images.push({ file: image, data });
          }
          const sourceSummary = sourceCount === null ? "" : `，参考 ${sourceCount} 条行业来源`;
          await wecomClient.sendText(task.userId, `《${title}》已完成，共 ${pageCount} 页${sourceSummary}。正在发送高清内参图片。`);
          for (const image of images) {
            const media = await wecomClient.uploadMedia({
              type: "image",
              filename: image.file.originalname || `创创今日内参-${task.id}.png`,
              data: image.data,
              mimeType: "image/png",
            });
            await wecomClient.sendImage(task.userId, { mediaId: media.mediaId });
          }
          await wecomClient.sendText(task.userId, `《${title}》${pageCount} 页内参已全部发送，可直接保存或转发。`);
        }
        const totalSize = uploadedFiles.reduce((sum, file) => sum + Number(file.size || 0), 0);
        const filenames = uploadedFiles.map((file) => file.originalname).filter(Boolean);
        const completed = await wecomContentTasks.complete(task.id, task.leaseId, {
          type,
          runId: cleanText(req.body?.runId, 160) || task.runId,
          title,
          filename: type === "video" ? videoFiles[0].originalname : undefined,
          filenames: type === "images" ? filenames : undefined,
          size: totalSize,
          pageCount,
          sourceCount,
          deliveredAt: new Date().toISOString(),
        });
        await Promise.all(uploadedFiles.map((file) => rm(file.path, { force: true })));
        res.json({ ok: true, task: completed });
      } catch (error) {
        await Promise.all(uploadedFiles.map((file) => rm(file.path, { force: true }).catch(() => {})));
        if (task?.id && task?.leaseId) {
          await wecomContentTasks.fail(task.id, task.leaseId, error?.message || "作品回传失败").catch(() => {});
          if (typeof wecomClient.sendText === "function") {
            await wecomClient.sendText(task.userId, "创创 AI 总调度官：作品回传遇到异常，本次任务已暂停且不会自动重跑。发送新指令即可开始下一次任务。").catch(() => {});
          }
        }
        next(error);
      }
    },
  );

  app.post("/workspace-api/wecom/content/tasks/:id/fail", requireWeComContentBridge, async (req, res, next) => {
    try {
      const task = await wecomContentTasks.fail(
        cleanText(req.params.id, 80),
        cleanText(req.body?.leaseId, 80),
        cleanText(req.body?.message, 400),
      );
      if (!task) throw Object.assign(new Error("任务租约已失效"), { statusCode: 409, code: "WECOM_TASK_LEASE_EXPIRED" });
      if (typeof wecomClient.sendText === "function") {
        await wecomClient.sendText(task.userId, `创创 AI 总调度官：生产遇到异常，本次任务已暂停且不会自动重跑。原因：${task.error}`);
      }
      res.json({ ok: true, task });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/auth/wecom/bootstrap", weComBootstrapLimiter, async (req, res, next) => {
    try {
      const expectedToken = (await readFile(weComBootstrapTokenPath, "utf8")).trim();
      if (!secureEqualText(req.get("x-bootstrap-token"), expectedToken)) {
        throw new AuthError("一次性配置凭证无效", "BOOTSTRAP_TOKEN_INVALID", 403);
      }
      const config = {
        corpId: cleanText(req.body?.corpId, 128),
        agentId: cleanText(req.body?.agentId, 20),
        secret: cleanText(req.body?.secret, 256),
        redirectUri: cleanText(req.body?.redirectUri, 300),
      };
      if (!/^[A-Za-z0-9_-]{3,128}$/u.test(config.corpId)
        || !/^\d{1,20}$/u.test(config.agentId)
        || !/^[A-Za-z0-9_-]{16,256}$/u.test(config.secret)
        || config.redirectUri !== "https://sail-crm.chuangchuang.cn/workspace-api/auth/wecom/callback") {
        throw Object.assign(new Error("企业微信配置无效"), { statusCode: 400, code: "WECOM_BOOTSTRAP_INVALID" });
      }
      await writeFile(weComBootstrapPendingPath, `${JSON.stringify(config)}\n`, { mode: 0o600, flag: "wx" });
      await rm(weComBootstrapTokenPath, { force: true });
      res.status(202).json({ ok: true, status: "accepted" });
    } catch (error) {
      if (error?.code === "ENOENT") return next(new AuthError("一次性配置入口未开启", "BOOTSTRAP_CLOSED", 404));
      if (error?.code === "EEXIST") return next(Object.assign(new Error("配置已接收，请等待生效"), { statusCode: 409, code: "BOOTSTRAP_ALREADY_ACCEPTED" }));
      return next(error);
    }
  });

  app.get("/workspace-api/auth/wecom/status", (_req, res) => {
    res.json({ ok: true, enabled: Boolean(wecomClient), autoProvision: Boolean(wecomClient && wecomConfig.autoProvision) });
  });

  app.get("/workspace-api/auth/wecom/start", loginIpLimiter, (req, res, next) => {
    try {
      if (!wecomClient) throw new WeComError("企业微信登录尚未启用", "WECOM_NOT_CONFIGURED", 503);
      const state = randomBytes(24).toString("base64url");
      const requestedMode = cleanText(req.query?.mode, 20).toLowerCase();
      const insideWeCom = /wxwork/iu.test(cleanText(req.get("user-agent"), 500));
      const useMobileLogin = requestedMode === "mobile" || insideWeCom;
      res.cookie(WECOM_STATE_COOKIE, state, wecomStateCookieOptions);
      if (useMobileLogin && typeof wecomClient.buildMobileLoginUrl === "function") {
        return res.redirect(302, wecomClient.buildMobileLoginUrl(state));
      }
      return res.redirect(302, wecomClient.buildLoginUrl(state));
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auth/wecom/callback", loginIpLimiter, async (req, res) => {
    const state = cleanText(req.query?.state, 300);
    const expectedState = req.cookies?.[WECOM_STATE_COOKIE];
    res.clearCookie(WECOM_STATE_COOKIE, {
      httpOnly: true,
      secure: secureCookies,
      sameSite: "lax",
      path: "/workspace-api/auth/wecom",
    });
    if (!wecomClient || !secureEqualText(state, expectedState)) {
      return res.redirect(303, "/workspace/?wecom_error=expired");
    }
    try {
      const identity = await wecomClient.identify(cleanText(req.query?.code, 1_024));
      const result = await auth.authenticateWeCom({
        wecomUserId: identity.userId,
        profile: identity,
        autoProvision: wecomConfig.autoProvision,
        ip: req.ip,
        userAgent: cleanText(req.get("user-agent"), 500),
      });
      setSessionCookie(res, result.token);
      return res.redirect(303, "/workspace/?wecom=success");
    } catch (error) {
      await store.addAudit({
        action: "auth.wecom_login_failed",
        actorUserId: null,
        targetType: "session",
        targetId: null,
        metadata: { code: error?.code || "WECOM_ERROR", ip: req.ip },
      }).catch(() => {});
      const code = error instanceof WeComError || error instanceof AuthError
        ? weComRedirectErrorCode(error)
        : "unavailable";
      return res.redirect(303, `/workspace/?wecom_error=${encodeURIComponent(code)}`);
    }
  });

  app.post("/workspace-api/auth/login", loginIpLimiter, loginLimiter, async (req, res, next) => {
    try {
      const result = await auth.authenticate({
        email: cleanText(req.body?.email, 240),
        password: String(req.body?.password || ""),
        ip: req.ip,
        userAgent: cleanText(req.get("user-agent"), 500),
      });
      setSessionCookie(res, result.token);
      res.json({ ok: true, user: serializeUser(result.user) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auth/local-preview", async (req, res, next) => {
    const hostname = String(req.hostname || "").replace(/^\[(.*)\]$/u, "$1");
    if (!localPreviewAuthEnabled || !["127.0.0.1", "localhost", "::1"].includes(hostname)) {
      return res.status(404).json({ ok: false, error: "Not found" });
    }
    try {
      const email = "local-preview@chuangchuang.local";
      let user = await store.getUserByEmail(email);
      if (!user) {
        const created = await auth.createUser({
          email,
          displayName: "本地预览",
          password: randomBytes(32).toString("base64url"),
          role: "admin",
        });
        user = await store.getUserById(created.id);
      }
      if (!user) throw new AuthError("本地预览账号创建失败", "LOCAL_PREVIEW_USER_FAILED", 503);
      const session = await auth.issueSession(user.id, {
        ip: req.ip,
        userAgent: cleanText(req.get("user-agent"), 500),
      });
      setSessionCookie(res, session.token);
      return res.json({ ok: true, user: serializeUser(user) });
    } catch (error) { return next(error); }
  });

  app.post("/workspace-api/auth/logout", async (req, res, next) => {
    try {
      const token = sessionToken(req);
      if (token) await auth.revokeSession(token).catch(() => false);
      res.clearCookie(COOKIE_NAME, { path: "/workspace-api" });
      res.json({ ok: true });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/speech/correct", requireUser, chatLimiter, async (req, res, next) => {
    try {
      const originalText = cleanText(req.body?.text, SPEECH_CORRECTION_MAX_LENGTH);
      if (!originalText) throw Object.assign(new Error("没有收到可纠正的语音文字"), { statusCode: 400, code: "SPEECH_TRANSCRIPT_REQUIRED" });
      const knowledgeDocuments = (await store.listKnowledgeDocuments({ status: "published" }))
        .filter((document) => canViewDocument(document, req.workspace.user));
      const glossary = buildSpeechGlossary({ knowledgeDocuments });
      const corrected = correctSpeechTranscript(originalText, {
        glossary,
        allowLowConfidence: false,
      });
      await store.addAudit({
        action: "workspace.speech_transcript_corrected",
        actorUserId: req.workspace.user.id,
        targetType: "speech_transcript",
        targetId: req.workspace.user.id,
        metadata: {
          correctionCount: corrected.corrections.length,
          appliedCount: corrected.corrections.filter((item) => item.applied).length,
          requiresConfirmation: corrected.requiresConfirmation,
          knowledgeGlossaryUsed: corrected.corrections.some((item) => item.source === "knowledge_glossary"),
        },
      });
      res.json({ ok: true, ...corrected });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auth/me", requireUser, (req, res) => {
    res.json({ ok: true, user: serializeUser(req.workspace.user) });
  });

  app.post("/workspace-api/auth/wecom/profile/sync", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      if (!wecomClient || typeof wecomClient.getProfile !== "function") {
        throw new WeComError("企业微信资料同步暂时不可用", "WECOM_NOT_CONFIGURED", 503);
      }
      const storedUser = await store.getUserById(req.workspace.user.id);
      if (!storedUser?.wecomUserId) {
        throw new AuthError("当前账号尚未绑定企业微信", "WECOM_NOT_LINKED", 409);
      }
      const profile = await wecomClient.getProfile(storedUser.wecomUserId);
      const user = await auth.syncWeComProfile(req.workspace.user.id, profile);
      res.json({ ok: true, user: serializeUser(user) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/departments", requireUser, async (_req, res) => {
    const departments = await store.listDepartments({ status: "active" });
    res.json({ ok: true, departments });
  });

  app.get("/workspace-api/directory", requireUser, async (_req, res) => {
    const sync = await syncWeComDirectory();
    const users = (await store.listUsers({ status: "active" }))
      .filter((user) => !user.wecomDirectoryManaged || user.wecomDirectoryStatus === "active")
      .map(serializeGroupMember)
      .filter(Boolean)
      .sort((left, right) => String(left.name || "").localeCompare(String(right.name || ""), "zh-CN"));
    res.json({
      ok: true,
      users,
      sync: {
        enabled: sync.enabled,
        status: sync.status,
        lastSuccessAt: sync.lastSuccessAt,
        count: sync.count,
        errorCode: sync.errorCode,
      },
    });
  });

  const getVisibleGroup = async (groupId, user) => {
    const group = await store.getGroup(groupId);
    if (!group || group.status !== "active" || !Array.isArray(group.memberIds) || !group.memberIds.includes(user.id)) {
      throw new AuthError("群组不存在或无权查看", "GROUP_FORBIDDEN", 404);
    }
    return group;
  };

  const addGroupAgentStatusMessage = async ({ group, agent, triggerMessage, status, content, citations = [], errorCode = null }) => store.addGroupMessage({
    groupId: group.id,
    senderUserId: null,
    senderType: "agent",
    agentId: agent.id,
    agentName: agent.label,
    agentIconSrc: agent.iconSrc,
    agentStatus: status,
    triggeredByMessageId: triggerMessage.id,
    content,
    citations,
    errorCode,
    attachmentIds: [],
  });

  const completeGroupAgentReply = async ({ group, agent, triggerMessage, placeholder, actor, policy }) => {
    const question = removeGroupAgentMentions(triggerMessage.content) || "请结合群聊上下文给出专业建议。";
    try {
      const recentMessages = (await store.listGroupMessages(group.id))
        .filter((message) => message.id !== placeholder.id)
        .slice(-16);
      const historyLines = recentMessages.map((message) => {
        const speaker = message.senderType === "agent"
          ? message.agentName || "公司智能体"
          : message.senderUserId === actor.id ? actor.displayName || actor.name || "当前员工" : "群成员";
        return `${speaker}：${redactSensitive(cleanText(message.content, 1_200))}`;
      });

      const knowledgeScope = agent.knowledgeScope;
      const effectiveModule = agent.module;
      const effectiveIntent = chooseHermesIntent({ prompt: question, module: effectiveModule });
      const shouldLoadCrmReadonly = knowledgeScope === CRM_EXPERT_SCOPE && effectiveIntent === "crm_query";
      const requestedCrmOutsideSalesExpert = knowledgeScope !== CRM_EXPERT_SCOPE
        && chooseHermesIntent({ prompt: question, module: moduleFor(question) }) === "crm_query";
      const requestedKnowledgeInsideSalesExpert = knowledgeScope === CRM_EXPERT_SCOPE
        && isExplicitKnowledgeQuestion(question);
      const shouldSearchKnowledge = KNOWLEDGE_ENABLED_SCOPES.has(knowledgeScope)
        && !requestedCrmOutsideSalesExpert;

      const today = new Date().toISOString().slice(0, 10);
      const userAudit = await store.listAudit({ actorUserId: actor.id });
      const usedToday = userAudit.filter((event) => ["workspace.chat_completed", "workspace.group_agent_completed"].includes(event.action)
        && event.createdAt?.startsWith(today)).length;
      if (usedToday >= Number(policy.dailyUserLimit || 100)) {
        throw Object.assign(new Error("今日任务额度已用完，请联系管理员"), { statusCode: 429, code: "DAILY_LIMIT_REACHED" });
      }

      const [documents, chunks, externalDocuments] = shouldSearchKnowledge
        ? await Promise.all([
          store.listKnowledgeDocuments({ status: "published" }),
          store.listKnowledgeChunks(),
          externalKnowledge.listDocuments().catch(() => []),
        ])
        : [[], [], []];
      const scoped = filterKnowledgeScope([
        ...buildKnowledgeDocuments(documents.filter((document) => !document.externalized), chunks),
        ...externalDocuments,
      ], knowledgeScope, actor);
      const explicitKnowledgeQuestion = shouldSearchKnowledge && isExplicitKnowledgeQuestion(question);
      const hits = shouldSearchKnowledge ? searchKnowledge(question.slice(0, 1_500), scoped, actor, { limit: 5 }) : [];
      let citations = hits.map((hit, index) => ({
        id: hit.documentId,
        title: hit.title,
        index: index + 1,
        source: hit.source,
        sourcePath: hit.sourcePath,
        sourceUpdatedAt: hit.sourceUpdatedAt,
      }));
      const contextSections = [
        `[群组协作场景]\n你正在“${group.name}”群中以“${agent.label}”身份参与。直接回应被邀请的问题，保持专业、简洁，并让群成员看得懂。`,
      ];
      if (historyLines.length) contextSections.push(`[本次群聊历史]\n${historyLines.join("\n")}`);
      const knowledgeContext = hits.map((hit, index) => `[资料${index + 1}] ${hit.title}\n${hit.excerpt}`).join("\n\n");
      if (knowledgeContext) contextSections.push(knowledgeContext);
      if (shouldLoadCrmReadonly) {
        if (!crmReadonly.enabled) {
          throw Object.assign(new Error("CRM只读查询尚未连接"), { statusCode: 503, code: "CRM_READONLY_NOT_CONFIGURED" });
        }
        let readonlyContext = null;
        if (crmReadonly.enabled) {
          try {
            readonlyContext = await crmReadonly.queryContext({
              prompt: question,
              user: actor,
              limit: actor.role === "admin" ? 10 : actor.role === "manager" ? 8 : 3,
              conversationContext: historyLines.join("\n"),
            });
          } catch (error) {
            if (error instanceof CrmReadonlyError) throw error;
            throw Object.assign(new Error("CRM只读查询暂时不可用，请稍后重试"), { statusCode: 503, code: "CRM_READONLY_UNAVAILABLE" });
          }
        }
        if (readonlyContext?.context) contextSections.push(`[CRM在线只读数据]\n${readonlyContext.context}`);
        const crmContext = await crm.buildAiContextSummary(actor, { limit: 20 });
        if (crmContext) contextSections.push(`[本人授权销售数据]\n${crmContext}`);
      }
      if (knowledgeScope === "内容与选题") {
        const topicHistory = (await store.listContentTopics({ status: "active" }))
          .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
          .slice(0, 30)
          .map((topic) => `- ${topic.title}${topic.corePoint ? `｜${topic.corePoint}` : ""}${topic.audience ? `｜受众：${topic.audience}` : ""}`)
          .join("\n");
        if (topicHistory) contextSections.push(`[公司近期内容选题，仅用于避免重复]\n${topicHistory}`);
      }

      const defaultModel = normalizeTextModelId(policy.defaultModel || policy.defaultMode);
      const model = knowledgeScope === CRM_EXPERT_SCOPE ? CRM_TEXT_MODEL_ID : defaultModel;
      const requestId = `workspace-group-${group.id}-${randomUUID()}`;
      const generated = requestedCrmOutsideSalesExpert
        ? {
          answer: "**当前助手未连接 CRM。**\n\n请在群里 @销售 CRM 专家 后重新查询。",
          route: "scope_guard",
          module: effectiveModule,
          intent: "crm_scope_required",
          profile: "",
          toolTrace: [],
        }
        : requestedKnowledgeInsideSalesExpert
          ? {
            answer: "**销售 CRM 专家只读取 CRM 在线数据。**\n\n需要公司知识时，请 @通用助手、@课程课件专家 或 @内容选题专家。",
            route: "scope_guard",
            module: effectiveModule,
            intent: "knowledge_scope_required",
            profile: "",
            toolTrace: [],
          }
          : await generate({
              model,
              prompt: question,
              context: contextSections.join("\n\n"),
              citations,
              hasFile: false,
              module: ["销售与 CRM", "课程课件", "内容与选题"].includes(effectiveModule) ? effectiveModule : undefined,
              user: actor,
              requestId,
              requireCitations: explicitKnowledgeQuestion && citations.length > 0,
            });
      const aligned = alignAnswerCitations(generated.answer, citations);
      citations = aligned.citations;
      const completed = await store.updateGroupMessage(placeholder.id, {
        content: aligned.answer,
        citations,
        agentStatus: "completed",
        route: generated.route,
        model: generated.model || model,
        errorCode: null,
      });
      await store.addAudit({
        action: "workspace.group_agent_completed",
        actorUserId: actor.id,
        targetType: "group",
        targetId: group.id,
        metadata: {
          agentId: agent.id,
          agentRequestId: requestId,
          triggerMessageId: triggerMessage.id,
          intent: generated.intent,
          citationCount: citations.length,
          crmReadonlyQueried: shouldLoadCrmReadonly,
        },
      });
      return completed;
    } catch (error) {
      const fallback = cleanText(safeError(error).message, 180) || "暂时无法响应，请稍后重试";
      const failed = await store.updateGroupMessage(placeholder.id, {
        content: `${agent.label}暂时无法完成这次请求：${fallback}`,
        agentStatus: "failed",
        errorCode: safeDiagnosticCode(error?.code, "GROUP_AGENT_FAILED"),
      });
      await store.addAudit({
        action: "workspace.group_agent_failed",
        actorUserId: actor.id,
        targetType: "group",
        targetId: group.id,
        metadata: {
          agentId: agent.id,
          triggerMessageId: triggerMessage.id,
          code: safeDiagnosticCode(error?.code, "GROUP_AGENT_FAILED"),
        },
      }).catch(() => {});
      return failed;
    }
  };

  app.get("/workspace-api/groups/agents", requireUser, (_req, res) => {
    res.json({ ok: true, agents: publicGroupAgents() });
  });

  app.get("/workspace-api/groups", requireUser, async (req, res) => {
    const [groups, users, messages, files] = await Promise.all([
      store.listGroups({ status: "active" }),
      store.listUsers(),
      store.listGroupMessages(),
      store.listFiles(),
    ]);
    const userMap = new Map(users.map((user) => [user.id, user]));
    const fileMap = new Map(files.map((file) => [file.id, file]));
    const visible = groups
      .filter((group) => Array.isArray(group.memberIds) && group.memberIds.includes(req.workspace.user.id))
      .map((group) => {
        const groupMessages = messages.filter((message) => message.groupId === group.id);
        const lastMessage = groupMessages.at(-1) || null;
        const readAt = group.readAtByUser?.[req.workspace.user.id] || group.createdAt;
        const unreadCount = groupMessages.filter((message) => message.senderUserId !== req.workspace.user.id && message.createdAt > readAt).length;
        return {
          id: group.id,
          name: group.name,
          ownerUserId: group.ownerUserId,
          members: group.memberIds.map((id) => serializeGroupMember(userMap.get(id))).filter(Boolean),
          memberCount: group.memberIds.length,
          lastMessage: lastMessage ? serializeGroupMessage(lastMessage, userMap, fileMap) : null,
          unreadCount,
          createdAt: group.createdAt,
          updatedAt: lastMessage?.createdAt || group.updatedAt,
        };
      })
      .sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
    res.json({ ok: true, groups: visible });
  });

  app.post("/workspace-api/groups", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const name = cleanText(req.body?.name, 40);
      if (name.length < 2) throw Object.assign(new Error("群名称至少需要 2 个字"), { statusCode: 400, code: "GROUP_NAME_REQUIRED" });
      const requestedIds = Array.isArray(req.body?.memberIds) ? req.body.memberIds.map((id) => cleanText(id, 80)) : [];
      const activeUsers = await store.listUsers({ status: "active" });
      const activeIds = new Set(activeUsers.map((user) => user.id));
      const memberIds = [...new Set([req.workspace.user.id, ...requestedIds.filter((id) => activeIds.has(id))])];
      if (memberIds.length > 50) throw Object.assign(new Error("一个群组最多 50 人"), { statusCode: 400, code: "GROUP_MEMBER_LIMIT" });
      const timestamp = new Date().toISOString();
      const group = await store.createGroup({
        name,
        ownerUserId: req.workspace.user.id,
        memberIds,
        readAtByUser: { [req.workspace.user.id]: timestamp },
      });
      await store.addAudit({
        action: "workspace.group_created",
        actorUserId: req.workspace.user.id,
        targetType: "group",
        targetId: group.id,
        metadata: { memberCount: memberIds.length },
      });
      res.status(201).json({ ok: true, group: { ...group, members: memberIds.map((id) => serializeGroupMember(activeUsers.find((user) => user.id === id))).filter(Boolean) } });
    } catch (error) { next(error); }
  });

  app.patch("/workspace-api/groups/:id", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const group = await getVisibleGroup(req.params.id, req.workspace.user);
      if (group.ownerUserId !== req.workspace.user.id && req.workspace.user.role !== "admin") {
        throw new AuthError("只有群主或管理员可以管理群组", "GROUP_MANAGE_FORBIDDEN", 403);
      }
      const patch = {};
      if (req.body?.name !== undefined) {
        const name = cleanText(req.body.name, 40);
        if (name.length < 2) throw Object.assign(new Error("群名称至少需要 2 个字"), { statusCode: 400 });
        patch.name = name;
      }
      if (req.body?.memberIds !== undefined) {
        const requestedIds = Array.isArray(req.body.memberIds) ? req.body.memberIds.map((id) => cleanText(id, 80)) : [];
        const activeUsers = await store.listUsers({ status: "active" });
        const activeIds = new Set(activeUsers.map((user) => user.id));
        const memberIds = [...new Set([group.ownerUserId, ...requestedIds.filter((id) => activeIds.has(id))])];
        if (memberIds.length > 50) throw Object.assign(new Error("一个群组最多 50 人"), { statusCode: 400 });
        patch.memberIds = memberIds;
      }
      const updated = await store.updateGroup(group.id, patch);
      await store.addAudit({ action: "workspace.group_updated", actorUserId: req.workspace.user.id, targetType: "group", targetId: group.id, metadata: { fields: Object.keys(patch) } });
      res.json({ ok: true, group: updated });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/groups/:id/messages", requireUser, async (req, res, next) => {
    try {
      await getVisibleGroup(req.params.id, req.workspace.user);
      const [messages, users, files] = await Promise.all([
        store.listGroupMessages(req.params.id),
        store.listUsers(),
        store.listFiles({ groupId: req.params.id }),
      ]);
      const userMap = new Map(users.map((user) => [user.id, user]));
      const fileMap = new Map(files.map((file) => [file.id, file]));
      res.json({ ok: true, messages: messages.slice(-300).map((message) => serializeGroupMessage(message, userMap, fileMap)) });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/groups/:id/messages", requireUser, groupMessageLimiter, async (req, res, next) => {
    try {
      const group = await getVisibleGroup(req.params.id, req.workspace.user);
      const content = cleanText(req.body?.content, 4_000);
      const attachmentIds = [...new Set((Array.isArray(req.body?.attachmentIds) ? req.body.attachmentIds : [])
        .map((id) => cleanText(id, 80))
        .filter(Boolean))];
      if (attachmentIds.length > 4) {
        throw Object.assign(new Error("一条消息最多发送 4 个附件"), { statusCode: 400, code: "GROUP_ATTACHMENT_LIMIT" });
      }
      if (!content && !attachmentIds.length) {
        throw Object.assign(new Error("请输入消息内容或选择附件"), { statusCode: 400, code: "GROUP_MESSAGE_REQUIRED" });
      }
      const mentionedAgents = parseGroupAgentMentions(content);
      const attachments = [];
      for (const fileId of attachmentIds) {
        const file = await store.getFile(fileId);
        if (!file
          || file.groupId !== group.id
          || file.ownerUserId !== req.workspace.user.id
          || file.status !== "ready"
          || file.groupMessageId) {
          throw new AuthError("附件不存在、已发送或无权使用", "GROUP_ATTACHMENT_FORBIDDEN", 404);
        }
        attachments.push(file);
      }
      const existing = await store.listGroupMessages(group.id);
      if (existing.length + 1 + mentionedAgents.length > 5_000) throw Object.assign(new Error("当前群聊记录已达上限，请新建群组继续讨论"), { statusCode: 409, code: "GROUP_MESSAGE_LIMIT" });
      const message = await store.addGroupMessage({
        groupId: group.id,
        senderUserId: req.workspace.user.id,
        senderType: "user",
        content,
        attachmentIds,
      });
      for (const file of attachments) await store.updateFile(file.id, { groupMessageId: message.id });
      await store.updateGroup(group.id, (current) => ({ readAtByUser: { ...(current.readAtByUser || {}), [req.workspace.user.id]: message.createdAt } }));
      const sender = await store.getUserById(req.workspace.user.id);
      const policies = mentionedAgents.length ? await store.listModelPolicies({ key: "default" }) : [];
      const policy = policies[0] || { defaultModel: DEFAULT_TEXT_MODEL_ID, dailyUserLimit: 100 };
      const queued = [];
      const runnable = [];
      const fingerprint = createHash("sha256").update(removeGroupAgentMentions(content, mentionedAgents).normalize("NFKC").toLowerCase()).digest("hex");
      for (const agent of mentionedAgents) {
        const permit = groupAgentTriggerGuard.acquire({
          userId: req.workspace.user.id,
          groupId: group.id,
          agentId: agent.id,
          fingerprint,
        });
        if (!permit.ok) {
          const feedback = permit.code === "duplicate"
            ? "相同问题刚刚已经提交，请等待上一条回复或稍后再试。"
            : permit.code === "busy"
              ? "正在处理群里的上一条请求，完成后即可再次邀请。"
              : "智能体调用较频繁，请稍后再试。";
          queued.push(await addGroupAgentStatusMessage({
            group,
            agent,
            triggerMessage: message,
            status: "failed",
            content: feedback,
            errorCode: `GROUP_AGENT_${permit.code.toUpperCase()}`,
          }));
          continue;
        }
        let placeholder;
        try {
          placeholder = await addGroupAgentStatusMessage({
            group,
            agent,
            triggerMessage: message,
            status: "processing",
            content: "正在处理这条群消息，请稍候。",
          });
        } catch (error) {
          permit.release();
          throw error;
        }
        queued.push(placeholder);
        runnable.push({ agent, placeholder, release: permit.release });
      }
      res.status(201).json({
        ok: true,
        message: serializeGroupMessage(
          message,
          new Map([[sender.id, sender]]),
          new Map(attachments.map((file) => [file.id, file])),
        ),
        agentMessages: queued.map((item) => serializeGroupMessage(item, new Map())),
        agent: mentionedAgents.length ? {
          status: runnable.length ? "processing" : "blocked",
          mentioned: mentionedAgents.map((agent) => ({ id: agent.id, label: agent.label })),
        } : null,
      });
      if (runnable.length) {
        void (async () => {
          for (const task of runnable) {
            try {
              await completeGroupAgentReply({
                group,
                agent: task.agent,
                triggerMessage: message,
                placeholder: task.placeholder,
                actor: req.workspace.user,
                policy,
              });
            } finally {
              task.release();
            }
          }
        })().catch((error) => logDiagnostic("error", "group_agent_background_failed", {
          code: safeDiagnosticCode(error?.code, "GROUP_AGENT_BACKGROUND_FAILED"),
          groupId: group.id,
        }));
      }
    } catch (error) { next(error); }
  });

  app.post(
    "/workspace-api/groups/:id/attachments",
    requireUser,
    uploadLimiter,
    reserveUploadStorage,
    upload.single("file"),
    async (req, res, next) => {
      let storedFile;
      try {
        const group = await getVisibleGroup(req.params.id, req.workspace.user);
        if (!req.file) throw Object.assign(new Error("请选择文件"), { statusCode: 400 });
        const malwareScanStatus = requireMalwareScan ? await scanUpload(req.file.path) : "not_required";
        const buffer = await readFile(req.file.path);
        const sha256 = createHash("sha256").update(buffer).digest("hex");
        const fileId = randomUUID();
        const fileCreatedAt = new Date().toISOString();
        const persistentStorage = await persistWorkspaceFile({
          fileId,
          ownerUserId: req.workspace.user.id,
          originalName: req.file.originalname,
          sourcePath: req.file.path,
          mimeType: req.file.mimetype,
          sha256,
          createdAt: fileCreatedAt,
        });
        storedFile = await store.createFile({
          id: fileId,
          createdAt: fileCreatedAt,
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          groupId: group.id,
          originalName: req.file.originalname,
          ...persistentStorage,
          mimeType: req.file.mimetype,
          size: req.file.size,
          sha256,
          scope: "group",
          space: `群组/${group.name}`,
          status: "ready",
          malwareScanStatus,
          submitAsCompanyKnowledge: false,
          workflowStatus: "ordinary_group_attachment",
        });
        if (persistentStorage.storageBackend === "oss") await rm(req.file.path, { force: true });
        await store.addAudit({
          action: "workspace.group_attachment_uploaded",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: storedFile.id,
          metadata: {
            groupId: group.id,
            size: storedFile.size,
            sha256,
            submitAsCompanyKnowledge: false,
          },
        });
        res.status(201).json({ ok: true, file: serializeGroupAttachment(storedFile) });
      } catch (error) {
        if (error.code === "MALWARE_DETECTED") {
          await store.addAudit({
            action: "workspace.file_malware_blocked",
            actorUserId: req.workspace.user.id,
            targetType: "file",
            targetId: null,
            metadata: { blocked: true, groupId: req.params.id },
          }).catch(() => {});
        }
        if (storedFile) await store.updateFile(storedFile.id, { status: "failed", errorCode: error.code || "GROUP_ATTACHMENT_FAILED" }).catch(() => {});
        if (req.file?.path) await rm(req.file.path, { force: true }).catch(() => {});
        next(error);
      }
    },
  );

  app.post("/workspace-api/groups/:id/read", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const group = await getVisibleGroup(req.params.id, req.workspace.user);
      const timestamp = new Date().toISOString();
      await store.updateGroup(group.id, (current) => ({ readAtByUser: { ...(current.readAtByUser || {}), [req.workspace.user.id]: timestamp } }));
      res.json({ ok: true, readAt: timestamp });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/model-policy", requireUser, async (_req, res) => {
    const policies = await store.listModelPolicies({ key: "default" });
    const policy = policies[0] || null;
    const models = await getTextModelStatus();
    res.json({
      ok: true,
      policy: policy ? {
        defaultModel: normalizeTextModelId(policy.defaultModel || policy.defaultMode),
        allowEmployeeSwitch: policy.allowEmployeeSwitch && TEXT_MODEL_SWITCH_ENABLED,
        dailyUserLimit: policy.dailyUserLimit,
        models,
      } : null,
    });
  });

  app.get("/workspace-api/crm/readonly/status", requireUser, (_req, res) => {
    res.json({
      ok: true,
      enabled: crmReadonly.enabled,
      mode: "read_only",
      writeback: false,
    });
  });

  app.post("/workspace-api/system/outreach-reviews", requireOutreachReviewIngest, mutationLimiter, async (req, res, next) => {
    try {
      const rawReviews = Array.isArray(req.body?.reviews) ? req.body.reviews : [req.body];
      if (!rawReviews.length || rawReviews.length > 100) {
        throw Object.assign(new Error("单次审核入箱数量需为 1 到 100 条"), { statusCode: 400, code: "OUTREACH_REVIEW_BATCH_INVALID" });
      }
      const normalized = rawReviews.map(normalizeOutreachReviewInput);
      const created = [];
      let reused = 0;
      for (const item of normalized) {
        const result = await store.createOutreachReview(item);
        if (result.reused) reused += 1;
        else {
          created.push(result.review);
          await store.addAudit({
            action: "crm.outreach_review_requested",
            actorUserId: null,
            targetType: "outreach_review",
            targetId: result.review.id,
            metadata: { source: result.review.source, campaignName: result.review.campaignName },
          });
        }
      }
      const notification = await notifyOutreachReviewAdmins(created);
      res.status(created.length ? 201 : 200).json({
        ok: true,
        created: created.length,
        reused,
        notified: notification.sent,
        reviewIds: created.map((review) => review.id),
      });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/admin/outreach-reviews", requireUser, requireAdmin, async (req, res, next) => {
    try {
      await refreshOutreachDeliveryStatuses().catch(() => {});
      const status = cleanText(req.query?.status, 80);
      const reviews = await store.listOutreachReviews();
      const visible = status && status !== "all"
        ? reviews.filter((review) => (
          review.status === status
          || review.approvalStatus === status
          || review.deliveryStatus === status
        ))
        : reviews;
      res.json({
        ok: true,
        enabled: crmOutreach.enabled,
        reviews: visible.map(serializeOutreachReview),
        counts: {
          pending: reviews.filter((review) => review.approvalStatus === "pending").length,
          approved: reviews.filter((review) => review.approvalStatus === "approved").length,
          rejected: reviews.filter((review) => review.approvalStatus === "rejected").length,
          awaitingStaff: reviews.filter((review) => review.deliveryStatus === "pending_staff_confirmation").length,
          sent: reviews.filter((review) => review.deliveryStatus === "sent").length,
          attention: reviews.filter((review) => review.deliveryStatus === "manual_attention").length,
        },
      });
    } catch (error) { next(error); }
  });

  app.patch("/workspace-api/admin/outreach-reviews/:id", requireUser, requireAdmin, mutationLimiter, async (req, res, next) => {
    try {
      if (req.body?.confirm !== true) {
        throw Object.assign(new Error("请完成二次确认"), { statusCode: 409, code: "EXPLICIT_CONFIRMATION_REQUIRED" });
      }
      const action = cleanText(req.body?.action, 40);
      if (!['approve', 'reject'].includes(action)) {
        throw Object.assign(new Error("审核动作无效"), { statusCode: 400, code: "OUTREACH_REVIEW_ACTION_INVALID" });
      }
      const existing = await store.getOutreachReview(req.params.id);
      if (!existing) {
        throw Object.assign(new Error("触达审核记录不存在"), { statusCode: 404, code: "OUTREACH_REVIEW_NOT_FOUND" });
      }
      if (action === 'reject') {
        if (existing.approvalStatus === 'approved') {
          throw Object.assign(new Error("该触达已经通过，不能改为拒绝"), { statusCode: 409, code: "OUTREACH_ALREADY_APPROVED" });
        }
        if (existing.approvalStatus === 'rejected') {
          res.json({ ok: true, reused: true, review: serializeOutreachReview(existing) });
          return;
        }
        const rejectionReason = cleanText(req.body?.reason, 500);
        if (!rejectionReason) {
          throw Object.assign(new Error("请填写拒绝原因"), { statusCode: 400, code: "OUTREACH_REJECTION_REASON_REQUIRED" });
        }
        const rejectedAt = new Date().toISOString();
        const rejected = await store.updateOutreachReview(existing.id, {
          status: 'rejected',
          approvalStatus: 'rejected',
          deliveryStatus: 'cancelled_before_submission',
          rejectionReason,
          rejectedByUserId: req.workspace.user.id,
          rejectedAt,
        });
        await store.addAudit({
          action: "crm.outreach_review_rejected",
          actorUserId: req.workspace.user.id,
          targetType: "outreach_review",
          targetId: existing.id,
          metadata: { reason: rejectionReason },
        });
        res.json({ ok: true, review: serializeOutreachReview(rejected) });
        return;
      }

      const content = cleanText(req.body?.content || existing.content, 4_000);
      if (!content) {
        throw Object.assign(new Error("待发送内容不能为空"), { statusCode: 400, code: "OUTREACH_CONTENT_REQUIRED" });
      }
      if (/承诺收益|保证成交|稳赚/iu.test(content)) {
        throw Object.assign(new Error("拟发送内容包含禁止承诺"), { statusCode: 409, code: "OUTREACH_FORBIDDEN_CLAIM" });
      }
      const claimed = await store.claimOutreachReviewForSubmission(existing.id, {
        actorUserId: req.workspace.user.id,
        content,
        note: cleanText(req.body?.note, 500),
      });
      if (!claimed.review) {
        throw Object.assign(new Error("触达审核记录不存在"), { statusCode: 404, code: "OUTREACH_REVIEW_NOT_FOUND" });
      }
      if (!claimed.claimed) {
        if (claimed.reason === 'already_approved') {
          res.json({ ok: true, reused: true, review: serializeOutreachReview(claimed.review) });
          return;
        }
        throw Object.assign(new Error("当前状态不能通过"), { statusCode: 409, code: "OUTREACH_REVIEW_STATE_CONFLICT" });
      }
      await store.addAudit({
        action: "crm.outreach_review_approved",
        actorUserId: req.workspace.user.id,
        targetType: "outreach_review",
        targetId: existing.id,
        metadata: { autoSubmit: true },
      });
      const submission = await submitApprovedOutreachReview(claimed.review);
      res.json({
        ok: true,
        submitted: submission.submitted,
        retryQueued: !submission.submitted && submission.retryable === true,
        review: serializeOutreachReview(submission.review),
      });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/crm/readonly/export", requireUser, artifactLimiter, async (req, res, next) => {
    let outputPath = "";
    try {
      if (!canExportCrm(req.workspace.user)) {
        throw new AuthError("当前账号没有CRM条件导出权限", "CRM_EXPORT_FORBIDDEN", 403);
      }
      if (typeof crmReadonly.exportCsv !== "function") {
        throw Object.assign(new Error("CRM条件导出尚未连接"), { statusCode: 503, code: "CRM_EXPORT_UNAVAILABLE" });
      }
      const result = await crmReadonly.exportCsv({
        query: req.body?.query,
        filterField: req.body?.filterField,
        filterValue: req.body?.filterValue,
        sort: req.body?.sort,
        limit: req.body?.limit,
        offset: req.body?.offset,
        purpose: req.body?.purpose,
        user: req.workspace.user,
      });
      const filenameBase = cleanText(req.body?.filename, 80).replace(/\.csv$/iu, "") || "CRM客户条件导出";
      const safeFilename = `${filenameBase.replace(/[\\/:*?"<>|]/gu, "-")}.csv`;
      const csvBuffer = Buffer.from(result.csv, "utf8");
      req.artifactReservation = await reserveStorageCapacity(req.workspace.user.id, csvBuffer.length);
      outputPath = join(artifactDir, `${randomUUID()}.csv`);
      await writeFile(outputPath, csvBuffer, { mode: 0o600 });
      const file = await store.transaction((draft) => {
        const timestamp = new Date().toISOString();
        const createdFile = {
          id: randomUUID(),
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          originalName: safeFilename,
          storagePath: outputPath,
          mimeType: "text/csv; charset=utf-8",
          size: csvBuffer.length,
          sha256: createHash("sha256").update(csvBuffer).digest("hex"),
          scope: "personal",
          space: "CRM条件导出",
          status: "ready",
          artifactType: "crm_filtered_export",
          createdAt: timestamp,
          updatedAt: timestamp,
        };
        draft.files.push(createdFile);
        draft.auditEvents.push({
          id: randomUUID(),
          action: "crm.filtered_exported",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: createdFile.id,
          metadata: {
            purpose: result.purpose,
            field: result.field,
            filterValue: result.filterValue,
            query: result.query,
            sort: result.sort,
            offset: result.offset,
            exportedCount: result.count,
            matchedTotal: result.total,
            asOf: result.asOf,
          },
          createdAt: timestamp,
          updatedAt: timestamp,
        });
        return createdFile;
      });
      res.status(201).json({ ok: true, file: serializeFile(file), count: result.count, total: result.total, asOf: result.asOf });
    } catch (error) {
      if (outputPath) await rm(outputPath, { force: true }).catch(() => {});
      next(error);
    } finally {
      await releaseStorageReservation(req.artifactReservation);
    }
  });

  app.get("/workspace-api/knowledge/source-status", requireUser, async (_req, res) => {
    const mirrorDir = String(process.env.COMPANY_KNOWLEDGE_MIRROR_DIR || "").trim();
    let externalMirrorReady = false;
    if (mirrorDir) {
      try {
        const resolved = await realpath(mirrorDir);
        await statfsImpl(resolved);
        externalMirrorReady = true;
      } catch {
        externalMirrorReady = false;
      }
    }
    const externalStatus = await externalKnowledge.status();
    res.json({
      ok: true,
      workspaceKnowledgeReady: true,
      externalMirrorReady: externalStatus.ready || externalMirrorReady,
      externalKnowledge: externalStatus,
    });
  });

  app.get("/workspace-api/crm/leads", requireUser, async (req, res, next) => {
    try {
      const filters = {};
      if (req.query.stage) filters.stage = cleanText(req.query.stage, 40);
      const leads = await crm.listLeads(req.workspace.user, filters);
      res.json({ ok: true, leads });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/crm/leads", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const lead = await crm.createLead(req.body || {}, req.workspace.user);
      res.status(201).json({ ok: true, lead });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/crm/leads/:id", requireUser, async (req, res, next) => {
    try {
      const lead = await crm.getLead(req.params.id, req.workspace.user);
      res.json({ ok: true, lead });
    } catch (error) { next(error); }
  });

  app.patch("/workspace-api/crm/leads/:id", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const lead = await crm.updateLead(req.params.id, req.body || {}, req.workspace.user);
      res.json({ ok: true, lead });
    } catch (error) { next(error); }
  });

  app.delete("/workspace-api/crm/leads/:id", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const result = await crm.deleteLead(req.params.id, req.workspace.user);
      res.json({ ok: true, result });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/crm/leads/:id/followups", requireUser, async (req, res, next) => {
    try {
      const followups = await crm.listFollowups(req.params.id, req.workspace.user);
      res.json({ ok: true, followups });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/crm/leads/:id/followups", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const followup = await crm.addFollowup(req.params.id, req.body || {}, req.workspace.user);
      const lead = await crm.getLead(req.params.id, req.workspace.user);
      res.status(201).json({ ok: true, followup, lead });
    } catch (error) { next(error); }
  });

  app.patch("/workspace-api/crm/followups/:id", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const followup = await crm.updateFollowup(req.params.id, req.body || {}, req.workspace.user);
      res.json({ ok: true, followup });
    } catch (error) { next(error); }
  });

  app.delete("/workspace-api/crm/followups/:id", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const result = await crm.deleteFollowup(req.params.id, req.workspace.user);
      res.json({ ok: true, result });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/crm/reminders", requireUser, async (req, res, next) => {
    try {
      const reminders = await crm.listOverdueFollowups(req.workspace.user, { limit: 100 });
      res.json({ ok: true, reminders });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/content/topics", requireUser, async (_req, res) => {
    const topics = (await store.listContentTopics({ status: "active" }))
      .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
      .slice(0, 300)
      .map(serializeContentTopic);
    res.json({ ok: true, topics });
  });

  app.post("/workspace-api/content/topics/check", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const topic = normalizeContentTopic(req.body);
      const history = await store.listContentTopics({ status: "active" });
      const result = findDuplicates(topic, history, { cooldownDays: 90 });
      await store.addAudit({
        action: "content.topic_checked",
        actorUserId: req.workspace.user.id,
        targetType: "content_topic",
        targetId: null,
        metadata: { status: result.status, matchCount: result.matches.length },
      });
      res.json({ ok: true, topic, result });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/content/topics", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const topic = normalizeContentTopic(req.body);
      const history = await store.listContentTopics({ status: "active" });
      const duplicateCheck = findDuplicates(topic, history, { cooldownDays: 90 });
      const canOverride = ["admin", "manager"].includes(req.workspace.user.role) && req.body?.overrideDuplicate === true;
      if (duplicateCheck.shouldBlock && !canOverride) {
        throw Object.assign(new Error("发现近期高度重复选题，请调整角度后再保存"), { statusCode: 409, code: "TOPIC_DUPLICATE" });
      }
      const created = await store.createContentTopic({
        ...topic,
        fingerprint: fingerprintTopic(topic).digest,
        ownerUserId: req.workspace.user.id,
        departmentId: req.workspace.user.departmentId || null,
        sourceType: "company_content",
        visibility: CONTENT_TOPIC_VISIBILITY,
      });
      await store.addAudit({
        action: "content.topic_created",
        actorUserId: req.workspace.user.id,
        targetType: "content_topic",
        targetId: created.id,
        metadata: { duplicateStatus: duplicateCheck.status, overridden: canOverride },
      });
      res.status(201).json({ ok: true, topic: serializeContentTopic(created), duplicateCheck });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/conversations", requireUser, async (req, res) => {
    const conversations = await store.listConversations({ ownerUserId: req.workspace.user.id });
    conversations.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
    res.json({
      ok: true,
      conversations: conversations.map((conversation) => ({
        ...conversation,
        processing: (activeConversationRequests.get(conversation.id) || 0) > 0,
      })),
    });
  });

  app.post("/workspace-api/conversations", requireUser, mutationLimiter, async (req, res) => {
    await ensureConversationCapacity(req.workspace.user.id);
    const conversation = await store.createConversation({
      ownerUserId: req.workspace.user.id,
      departmentId: req.workspace.user.departmentId || null,
      title: cleanText(req.body?.title, 80) || "新对话",
    });
    res.status(201).json({ ok: true, conversation });
  });

  app.delete("/workspace-api/conversations/:id", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const conversation = await store.getConversation(req.params.id);
      if (!conversation || conversation.ownerUserId !== req.workspace.user.id) {
        throw new AuthError("对话不存在或无权删除", "CONVERSATION_FORBIDDEN", 404);
      }
      if ((activeConversationRequests.get(conversation.id) || 0) > 0) {
        throw Object.assign(new Error("这条对话正在生成内容，请完成后再删除"), { statusCode: 409, code: "CONVERSATION_BUSY" });
      }
      const deleted = await store.deleteOwnedConversation(req.params.id, req.workspace.user.id);
      if (!deleted) {
        throw new AuthError("对话不存在或无权删除", "CONVERSATION_FORBIDDEN", 404);
      }
      if (deleted.blocked) {
        throw Object.assign(new Error("这条对话有图片任务正在处理，请完成后再删除"), { statusCode: 409, code: "CONVERSATION_BUSY" });
      }
      res.json({ ok: true, deleted: true, conversationId: deleted.conversation.id });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/conversations/:id/messages", requireUser, async (req, res, next) => {
    try {
      const conversation = await store.getConversation(req.params.id);
      if (!conversation || conversation.ownerUserId !== req.workspace.user.id) {
        throw new AuthError("对话不存在或无权查看", "CONVERSATION_FORBIDDEN", 404);
      }
      res.json({ ok: true, messages: await store.listMessages(conversation.id) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auto-cut/templates", requireUser, (_req, res) => {
    res.json({ ok: true, templates: autoCutTemplateList() });
  });

  const runAutoCutInBackground = (runId) => {
    setImmediate(async () => {
      let run = await autoCutRuns.update(runId, { status: "RUNNING", updatedAt: new Date().toISOString() });
      if (!run) return;
      const onStage = async (event) => {
        run = await autoCutRuns.update(runId, (current) => advanceAutoCutStage(current, event));
        return run;
      };
      try {
        if (typeof executeAutoCutRun !== "function") {
          await onStage({ stageId: "transcribe", status: "failed", error: "云端智能剪辑执行器尚未配置" });
          return;
        }
        const output = await executeAutoCutRun({ run, onStage, dataDir, artifactDir, generate: generateAutoCutPlan });
        await autoCutRuns.update(runId, (current) => ({
          ...current,
          status: "WAITING_REVIEW",
          preview: {
            path: resolve(output.previewPath),
            fileName: cleanText(output.fileName, 300) || "晏总访谈-预览.mp4",
            sha256: cleanText(output.sha256, 64),
            sizeBytes: Math.max(0, Number(output.sizeBytes) || 0),
            durationS: Number(output.durationS) || null,
            width: Number(output.width) || null,
            height: Number(output.height) || null,
          },
          plan: output.plan || current.plan || null,
          updatedAt: new Date().toISOString(),
        }));
      } catch (error) {
        await onStage({ stageId: error?.stageId || "verify", status: "failed", error: error?.message || "智能剪辑执行失败" }).catch(() => {});
      }
    });
  };

  if (typeof autoCutRuns.listRecoverable === "function") {
    const recoverableRuns = await autoCutRuns.listRecoverable();
    for (const recoverableRun of recoverableRuns) runAutoCutInBackground(recoverableRun.id);
  }

  app.post("/workspace-api/auto-cut/runs", requireUser, chatLimiter, async (req, res, next) => {
    try {
      const templateKey = cleanText(req.body?.templateKey, 80) || AUTO_CUT_TEMPLATE_KEY;
      if (!getAutoCutTemplate(templateKey)) throw Object.assign(new Error("当前只支持晏总访谈模板"), { statusCode: 400, code: "AUTO_CUT_TEMPLATE_NOT_FOUND" });
      const prompt = redactSensitive(cleanText(req.body?.prompt, 6_000));
      if (!prompt) throw Object.assign(new Error("请先说清楚你希望这条视频怎么剪"), { statusCode: 400, code: "AUTO_CUT_PROMPT_REQUIRED" });
      const source = normalizeAutoCutSource(req.body?.source || {});
      if (["nas", "oss"].includes(source.type) && !AUTO_CUT_VIDEO_EXTENSIONS.has(extname(source.path).toLowerCase())) {
        throw Object.assign(new Error("原片仅支持 MP4、MOV、M4V 或 WEBM 视频"), { statusCode: 415, code: "AUTO_CUT_VIDEO_REQUIRED" });
      }
      if (source.type === "oss") source.path = normalizeAutoCutOssPath(source.path);
      const run = await autoCutRuns.create({ ownerUserId: req.workspace.user.id, source, prompt, templateKey });
      res.status(202).json({ ok: true, run: publicAutoCutRun(run) });
      runAutoCutInBackground(run.id);
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auto-cut/runs", requireUser, async (req, res, next) => {
    try {
      const runs = await autoCutRuns.list(req.workspace.user.id);
      res.json({ ok: true, runs: runs.slice(0, 50).map(publicAutoCutRun) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auto-cut/runs/:id", requireUser, async (req, res, next) => {
    try {
      const run = await autoCutRuns.get(req.params.id);
      if (!run || run.ownerUserId !== req.workspace.user.id) throw Object.assign(new Error("剪辑任务不存在或无权查看"), { statusCode: 404, code: "AUTO_CUT_RUN_NOT_FOUND" });
      res.json({ ok: true, run: publicAutoCutRun(run) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auto-cut/runs/:id/preview", requireUser, async (req, res, next) => {
    try {
      const run = await autoCutRuns.get(req.params.id);
      if (!run || run.ownerUserId !== req.workspace.user.id || !run.preview?.path) throw Object.assign(new Error("预览不存在或无权查看"), { statusCode: 404, code: "AUTO_CUT_PREVIEW_NOT_FOUND" });
      res.type("video/mp4");
      res.sendFile(resolve(run.preview.path));
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/auto-cut/oss/healthz", requireUser, (_req, res) => {
    res.json({ ok: true, configured: autoCutOssConfigured(AUTO_CUT_OSS_CONFIG), source: "oss", prefix: AUTO_CUT_OSS_CONFIG.prefix });
  });

  app.get("/workspace-api/auto-cut/oss/list", requireUser, async (req, res, next) => {
    try {
      const listing = await listAutoCutOssDirectory(req.query.path || "", AUTO_CUT_OSS_CONFIG);
      res.json({ ok: true, source: "oss", ...listing });
    } catch (error) {
      next(error);
    }
  });

  app.get("/workspace-api/auto-cut/oss/file", requireUser, async (req, res, next) => {
    try {
      res.redirect(302, await getAutoCutOssSignedFileUrl(req.query.path || "", AUTO_CUT_OSS_CONFIG));
    } catch (error) {
      next(error);
    }
  });

  app.post("/workspace-api/auto-cut/sources", requireUser, uploadLimiter, autoCutVideoUpload.single("file"), async (req, res, next) => {
    let reservation;
    let storedFile;
    let uploadClaimed = false;
    const userId = req.workspace.user.id;
    try {
      if (!req.file) throw Object.assign(new Error("请选择要剪辑的原片"), { statusCode: 400, code: "AUTO_CUT_SOURCE_REQUIRED" });
      if (req.body?.confirmCompanyOwned !== "true") {
        throw Object.assign(new Error("请确认原片属于创创内部办公素材"), { statusCode: 400, code: "AUTO_CUT_SOURCE_SCOPE_CONFIRMATION_REQUIRED" });
      }
      if (activeUploadUsers.has(userId)) {
        throw Object.assign(new Error("已有原片正在上传，请等待完成后再试"), { statusCode: 409, code: "AUTO_CUT_UPLOAD_ALREADY_RUNNING" });
      }
      activeUploadUsers.add(userId);
      uploadClaimed = true;
      activeUploadCount += 1;
      reservation = await reserveStorageCapacity(userId, Number(req.file.size) || 0);
      const malwareScanStatus = requireMalwareScan ? await scanUpload(req.file.path) : "not_required";
      const sha256 = await sha256File(req.file.path);
      storedFile = await store.createFile({
        ownerUserId: userId,
        departmentId: req.workspace.user.departmentId || null,
        originalName: req.file.originalname,
        storagePath: req.file.path,
        mimeType: req.file.mimetype,
        size: req.file.size,
        sha256,
        scope: "personal",
        space: "智能剪辑",
        status: "ready",
        malwareScanStatus,
        submitAsCompanyKnowledge: false,
        workflowStatus: "auto_cut_source",
        artifactType: "auto_cut_source",
      });
      await store.addAudit({
        action: "workspace.auto_cut_source_uploaded",
        actorUserId: userId,
        targetType: "file",
        targetId: storedFile.id,
        metadata: { size: storedFile.size, sha256, mimeType: storedFile.mimeType },
      });
      res.status(201).json({
        ok: true,
        file: serializeFile(storedFile),
        source: { type: "upload", fileId: storedFile.id, name: storedFile.originalName },
      });
    } catch (error) {
      if (storedFile) await store.updateFile(storedFile.id, { status: "failed", errorCode: error.code || "AUTO_CUT_SOURCE_FAILED" }).catch(() => {});
      if (req.file?.path && !storedFile) await rm(req.file.path, { force: true }).catch(() => {});
      next(error);
    } finally {
      if (uploadClaimed) {
        activeUploadUsers.delete(userId);
        activeUploadCount = Math.max(0, activeUploadCount - 1);
      }
      await releaseStorageReservation(reservation).catch(() => {});
    }
  });

  app.post("/workspace-api/auto-cut/plan", requireUser, chatLimiter, async (req, res, next) => {
    try {
      const templateKey = cleanText(req.body?.templateKey, 80) || AUTO_CUT_TEMPLATE_KEY;
      const template = getAutoCutTemplate(templateKey);
      if (!template) {
        throw Object.assign(new Error("当前只支持晏总访谈模板"), { statusCode: 400, code: "AUTO_CUT_TEMPLATE_NOT_FOUND" });
      }
      const userPrompt = redactSensitive(cleanText(req.body?.prompt, 6_000));
      if (!userPrompt) {
        throw Object.assign(new Error("请先说清楚你希望这条视频怎么剪"), { statusCode: 400, code: "AUTO_CUT_PROMPT_REQUIRED" });
      }
      let source;
      try {
        source = normalizeAutoCutSource(req.body?.source || {
          type: "nas",
          path: req.body?.sourcePath,
          name: req.body?.videoName,
        });
      } catch {
        throw Object.assign(new Error("请选择 OSS/NAS 原片或上传一条原片"), { statusCode: 400, code: "AUTO_CUT_SOURCE_REQUIRED" });
      }
      if (source.type === "oss") {
        if (!autoCutOssConfigured(AUTO_CUT_OSS_CONFIG)) {
          throw Object.assign(new Error("自动剪辑 OSS 原片库尚未配置"), { statusCode: 503, code: "AUTO_CUT_OSS_NOT_CONFIGURED" });
        }
        source.path = normalizeAutoCutOssPath(source.path);
      }
      if (["nas", "oss"].includes(source.type) && !AUTO_CUT_VIDEO_EXTENSIONS.has(extname(source.path).toLowerCase())) {
        throw Object.assign(new Error("OSS/NAS 原片仅支持 MP4、MOV、M4V 或 WEBM 视频"), { statusCode: 415, code: "AUTO_CUT_VIDEO_REQUIRED" });
      }
      let sourceFile = null;
      if (source.type === "upload") {
        sourceFile = await store.getFile(source.fileId);
        const allowedUpload = sourceFile
          && sourceFile.ownerUserId === req.workspace.user.id
          && sourceFile.status === "ready"
          && sourceFile.workflowStatus === "auto_cut_source"
          && (AUTO_CUT_VIDEO_MIME_TYPES.has(sourceFile.mimeType) || sourceFile.mimeType === "application/octet-stream");
        if (!allowedUpload) {
          throw Object.assign(new Error("上传的原片不存在、未完成安全检查或无权使用"), { statusCode: 404, code: "AUTO_CUT_SOURCE_FORBIDDEN" });
        }
      }
      const sourcePath = ["nas", "oss"].includes(source.type) ? source.path : null;
      const videoName = redactSensitive(source.name || sourceFile?.originalName || "上传原片");
      const requestId = safeRequestId(req.body?.requestId || `auto-cut-${randomUUID()}`);
      const generated = await generate({
        model: DEFAULT_TEXT_MODEL_ID,
        prompt: buildAutoCutPrompt({
          userPrompt,
          videoName,
          transcript: redactSensitive(cleanText(req.body?.transcript, 18_000)),
        }),
        context: "",
        citations: [],
        module: "内容与选题",
        user: req.workspace.user,
        requestId,
        orchestrationRequestKey: `auto-cut:${requestId}`,
        langGraphAdapter,
      });
      const plan = parseAutoCutPlan(generated?.answer);
      await store.addAudit({
        action: "workspace.auto_cut_plan_generated",
        actorUserId: req.workspace.user.id,
        targetType: "auto_cut_template",
        targetId: `${template.key}@${template.version}`,
        metadata: {
          templateKey: template.key,
          templateVersion: template.version,
          videoName,
          sourceType: source.type,
          sourcePath,
          sourceFileId: source.fileId || null,
          model: generated?.model || generated?.route || DEFAULT_TEXT_MODEL_ID,
          segmentCount: plan.selectedSegments.length,
        },
      });
      res.json({
        ok: true,
        template,
        source: ["nas", "oss"].includes(source.type)
          ? { type: source.type, path: sourcePath, name: videoName || sourcePath.split("/").at(-1) || "OSS 原片" }
          : { type: "upload", fileId: source.fileId, name: videoName },
        plan,
        agent: {
          status: "completed",
          model: generated?.model || generated?.route || DEFAULT_TEXT_MODEL_ID,
          route: generated?.route || null,
          executionPath: generated?.executionPath || null,
        },
        nextAction: "preview_render",
      });
    } catch (error) { next(error); }
  });

  const taskStatuses = new Set([
    "QUEUED",
    "RUNNING",
    "WAITING_REVIEW",
    "APPROVED",
    "REJECTED",
    "FAILED",
    "CANCELLED",
  ]);
  const taskCapabilities = new Set([
    "general_office",
    "sales_crm",
    "courseware",
    "content",
    "design",
  ]);
  const ownedHarnessTask = async (taskId, user) => {
    const snapshot = await agentHarness.getTask(taskId);
    const task = snapshot?.task;
    if (!task || String(task.actor_user_id || "") !== String(user.id || "")) {
      throw new AuthError("任务不存在或无权查看", "WORKSPACE_TASK_FORBIDDEN", 404);
    }
    return snapshot;
  };

  app.get("/workspace-api/tasks", requireUser, async (req, res, next) => {
    try {
      if (!agentHarness.enabled) {
        res.json({ ok: true, configured: false, runtime: agentHarness.runtime || "hermes", count: 0, tasks: [] });
        return;
      }
      const requestedStatus = cleanText(req.query?.status, 40).toUpperCase();
      const requestedCapability = cleanText(req.query?.capability, 80);
      const status = taskStatuses.has(requestedStatus) ? requestedStatus : null;
      const capability = taskCapabilities.has(requestedCapability) ? requestedCapability : null;
      const limit = Math.max(1, Math.min(100, Number(req.query?.limit) || 50));
      const result = await agentHarness.listTasks({
        actorUserId: req.workspace.user.id,
        status,
        capability,
        limit,
      });
      const tasks = (Array.isArray(result?.tasks) ? result.tasks : []).map(publicWorkspaceTask);
      res.json({ ok: true, configured: true, runtime: agentHarness.runtime || "hermes", count: tasks.length, tasks });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/tasks/:id", requireUser, async (req, res, next) => {
    try {
      if (!agentHarness.enabled) {
        res.json({ ok: true, configured: false, runtime: agentHarness.runtime || "hermes", task: null, events: [], reviews: [] });
        return;
      }
      const snapshot = await ownedHarnessTask(req.params.id, req.workspace.user);
      res.json({ ok: true, configured: true, runtime: agentHarness.runtime || "hermes", ...publicWorkspaceTaskDetail(snapshot) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/tasks/:id/events", requireUser, async (req, res, next) => {
    try {
      if (!agentHarness.enabled) {
        throw Object.assign(new Error("任务中枢尚未连接"), { statusCode: 503, code: "AGENT_HARNESS_NOT_CONFIGURED" });
      }
      await ownedHarnessTask(req.params.id, req.workspace.user);
      res.status(200);
      res.set({
        "content-type": "text/event-stream; charset=utf-8",
        "cache-control": "no-cache, no-transform",
        connection: "keep-alive",
        "x-accel-buffering": "no",
      });
      res.flushHeaders?.();
      let closed = false;
      let refreshing = false;
      let lastVersion = "";
      const writeSnapshot = async () => {
        if (closed || refreshing) return;
        refreshing = true;
        try {
          const snapshot = await ownedHarnessTask(req.params.id, req.workspace.user);
          const detail = publicWorkspaceTaskDetail(snapshot);
          const signature = `${detail.task.version}:${detail.events.at(-1)?.sequence || 0}:${detail.reviews.length}`;
          if (signature === lastVersion) return;
          lastVersion = signature;
          res.write(`id: ${signature}\n`);
          res.write("event: task\n");
          res.write(`data: ${JSON.stringify(detail)}\n\n`);
        } catch (error) {
          res.write("event: error\n");
          res.write(`data: ${JSON.stringify({ code: error?.code || "TASK_STREAM_FAILED", message: "任务进度暂时无法刷新" })}\n\n`);
        } finally {
          refreshing = false;
        }
      };
      await writeSnapshot();
      const timer = setInterval(writeSnapshot, 2_500);
      const keepalive = setInterval(() => { if (!closed) res.write(": keepalive\n\n"); }, 15_000);
      req.on("close", () => {
        closed = true;
        clearInterval(timer);
        clearInterval(keepalive);
      });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/tasks/:id/review", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      if (!agentHarness.enabled) {
        throw Object.assign(new Error("任务中枢尚未连接"), { statusCode: 503, code: "AGENT_HARNESS_NOT_CONFIGURED" });
      }
      const snapshot = await ownedHarnessTask(req.params.id, req.workspace.user);
      const decision = req.body?.decision === "reject"
        ? "reject"
        : req.body?.decision === "approve" ? "approve" : null;
      if (!decision) {
        throw Object.assign(new Error("请选择验收结果"), { statusCode: 400, code: "TASK_REVIEW_DECISION_REQUIRED" });
      }
      const reviewed = await agentHarness.reviewTask(req.params.id, {
        decision,
        taskVersion: Number(req.body?.taskVersion) || Number(snapshot.task.version),
        reviewerUserId: req.workspace.user.id,
        reasonCodes: Array.isArray(req.body?.reasonCodes) ? req.body.reasonCodes.slice(0, 20) : [],
        note: cleanText(req.body?.note, 2_000) || null,
      });
      await store.addAudit({
        action: "workspace.task_reviewed",
        actorUserId: req.workspace.user.id,
        targetType: "workspace_task",
        targetId: req.params.id,
        metadata: { decision, runtime: agentHarness.runtime || "hermes" },
      });
      const nextSnapshot = await ownedHarnessTask(req.params.id, req.workspace.user).catch(() => reviewed);
      res.json({ ok: true, configured: true, runtime: agentHarness.runtime || "hermes", ...publicWorkspaceTaskDetail(nextSnapshot) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/chat/progress/:requestId", requireUser, (req, res, next) => {
    try {
      const progress = chatProgressHub.snapshot(req.workspace.user.id, req.params.requestId);
      if (!progress) {
        throw new AuthError("任务进度不存在或无权查看", "CHAT_PROGRESS_NOT_FOUND", 404);
      }
      res.setHeader("cache-control", "private, no-store, max-age=0");
      res.json({ ok: true, progress });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/chat/progress/:requestId/events", requireUser, (req, res, next) => {
    try {
      const userId = req.workspace.user.id;
      const requestId = req.params.requestId;
      chatProgressHub.open(userId, requestId);
      res.status(200);
      res.set({
        "content-type": "text/event-stream; charset=utf-8",
        "cache-control": "no-cache, no-transform",
        connection: "keep-alive",
        "x-accel-buffering": "no",
      });
      res.flushHeaders?.();
      let closed = false;
      const writeEvent = (event) => {
        if (closed || res.writableEnded) return;
        res.write(`id: ${event.sequence}\n`);
        res.write("event: progress\n");
        res.write(`data: ${JSON.stringify(event)}\n\n`);
        if (["COMPLETED", "FAILED"].includes(event.phase)) res.end();
      };
      const existing = chatProgressHub.snapshot(userId, requestId);
      for (const event of existing?.events || []) writeEvent(event);
      if (existing?.terminal) return;
      const unsubscribe = chatProgressHub.subscribe(userId, requestId, writeEvent);
      const keepalive = setInterval(() => {
        if (!closed && !res.writableEnded) res.write(": keepalive\n\n");
      }, 15_000);
      keepalive.unref();
      req.on("close", () => {
        closed = true;
        clearInterval(keepalive);
        unsubscribe?.();
      });
    } catch (error) { next(error); }
  });

  app.post(
    "/workspace-api/chat/attachments",
    requireUser,
    uploadLimiter,
    reserveUploadStorage,
    upload.single("file"),
    async (req, res, next) => {
      let storedFile;
      try {
        if (!req.file) throw Object.assign(new Error("请选择文件"), { statusCode: 400 });
        const malwareScanStatus = requireMalwareScan ? await scanUpload(req.file.path) : "not_required";
        const buffer = await readFile(req.file.path);
        const sha256 = createHash("sha256").update(buffer).digest("hex");
        const extension = extname(req.file.originalname || "").toLowerCase();
        const isImage = GROUP_IMAGE_MIME_BY_EXTENSION.has(extension);
        let extractedText = "";
        if (!isImage) {
          const extracted = await extractAndChunkDocument(
            { name: req.file.originalname, mimeType: req.file.mimetype, buffer },
            { idPrefix: `chat-${randomUUID()}` },
          );
          extractedText = redactSensitive(extracted.text);
          if (!extractedText) {
            throw Object.assign(new Error("文件没有可识别的文字内容"), { statusCode: 422, code: "CHAT_ATTACHMENT_EMPTY" });
          }
          if (containsRestrictedBusinessContent(extracted.text)) {
            throw Object.assign(new Error("该资料不在员工办公范围内"), { statusCode: 403, code: "KNOWLEDGE_SCOPE_DENIED" });
          }
        }
        const fileId = randomUUID();
        const fileCreatedAt = new Date().toISOString();
        const persistentStorage = isImage
          ? { storageBackend: "server_temp", storagePath: req.file.path }
          : await persistWorkspaceFile({
              fileId,
              ownerUserId: req.workspace.user.id,
              originalName: req.file.originalname,
              sourcePath: req.file.path,
              mimeType: req.file.mimetype,
              sha256,
              createdAt: fileCreatedAt,
            });
        storedFile = await store.createFile({
          id: fileId,
          createdAt: fileCreatedAt,
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          originalName: req.file.originalname,
          ...persistentStorage,
          mimeType: req.file.mimetype,
          size: req.file.size,
          sha256,
          scope: "personal",
          space: "智能体对话附件",
          status: "ready",
          malwareScanStatus,
          submitAsCompanyKnowledge: false,
          workflowStatus: "ordinary_chat_attachment",
          extractedText,
          chatAttachmentUsed: false,
          temporaryCache: isImage,
          cacheExpiresAt: isImage ? new Date(Date.now() + imageReferenceCacheTtlMs).toISOString() : null,
        });
        if (persistentStorage.storageBackend === "oss") await rm(req.file.path, { force: true });
        await store.addAudit({
          action: "workspace.chat_attachment_uploaded",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: storedFile.id,
          metadata: { size: storedFile.size, sha256, isImage, submitAsCompanyKnowledge: false },
        });
        res.status(201).json({ ok: true, file: serializeChatAttachment(storedFile) });
      } catch (error) {
        if (storedFile) {
          await store.updateFile(storedFile.id, { status: "failed", errorCode: error.code || "CHAT_ATTACHMENT_FAILED" }).catch(() => {});
        }
        if (req.file?.path) await rm(req.file.path, { force: true }).catch(() => {});
        next(error);
      }
    },
  );

  app.get("/workspace-api/images/status", requireUser, (_req, res) => {
    res.json({
      ok: true,
      enabled: imageEnabled,
      provider: imageProvider,
      jobTimeoutMs: lovartRemoteEnabled ? imageRemoteJobTimeoutMs : null,
      modes: imageClient.modes || Object.values(IMAGE_MODES).map(({ id, label, note }) => ({ id, label, note })),
      models: Object.values(IMAGE_MODELS).map(({ id, label, provider }) => ({
        id,
        label,
        provider,
        enabled: libTvImageEnabled,
      })),
      creditCost: imageCreditCost,
      videoEnabled: false,
    });
  });

  app.get("/workspace-api/credits", requireUser, async (req, res, next) => {
    try {
      const account = await store.ensureCreditAccount(req.workspace.user.id, imageInitialCredits);
      const transactions = await store.listCreditTransactions({ userId: req.workspace.user.id });
      res.json({
        ok: true,
        balance: account.balance,
        imageGenerationCost: imageCreditCost,
        costPolicy: CREDIT_POLICY,
        recharge: {
          ...getRechargeCatalog({ enabled: rechargeEnabled }),
          channels: rechargeGateway.status.channels,
        },
        transactions: transactions.slice(0, 20),
      });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/credits/orders", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      if (!rechargeEnabled) {
        throw Object.assign(new Error("微信和支付宝支付尚未同时完成配置，当前不会增加余额"), {
          statusCode: 503,
          code: rechargeRequested ? "PAYMENT_GATEWAY_NOT_READY" : "RECHARGE_NOT_CONFIGURED",
        });
      }
      const packageId = cleanText(req.body?.packageId, 80);
      const channel = cleanText(req.body?.channel, 20).toLowerCase();
      const idempotencyKey = cleanText(req.body?.idempotencyKey, 100) || null;
      const catalog = getRechargeCatalog({ enabled: true });
      const packageItem = catalog.packages.find((item) => item.id === packageId);
      if (!packageItem || !Number.isInteger(packageItem.cashAmountCents) || packageItem.cashAmountCents <= 0) {
        throw Object.assign(new Error("充值套餐不存在"), { statusCode: 400, code: "RECHARGE_PACKAGE_INVALID" });
      }
      if (!rechargeGateway.status.channels[channel]?.ready) {
        throw Object.assign(new Error("该支付渠道尚未完成服务端配置"), { statusCode: 503, code: "PAYMENT_CHANNEL_NOT_READY" });
      }
      await store.ensureCreditAccount(req.workspace.user.id, imageInitialCredits);
      const pending = await store.createPaymentOrder({
        userId: req.workspace.user.id,
        packageId: packageItem.id,
        packageName: packageItem.name,
        credits: packageItem.credits,
        amountCents: packageItem.cashAmountCents,
        currency: catalog.currency,
        channel,
        outTradeNo: newPaymentOrderNo(),
        idempotencyKey,
      });
      if (pending.packageId !== packageItem.id || pending.channel !== channel || pending.amountCents !== packageItem.cashAmountCents) {
        throw Object.assign(new Error("幂等请求与原充值订单内容不一致"), { statusCode: 409, code: "PAYMENT_IDEMPOTENCY_CONFLICT" });
      }
      if (pending.payment) {
        return res.json({ ok: true, order: serializePaymentOrder(pending), payment: pending.payment });
      }
      const payment = await rechargeGateway.createPayment({ channel, order: pending });
      const order = await store.updatePaymentOrder(pending.id, {
        status: "awaiting_payment",
        providerOrderId: payment.providerOrderId || null,
        payment,
      });
      res.status(201).json({ ok: true, order: serializePaymentOrder(order), payment });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/payments/wechat/notify", async (req, res, next) => {
    try {
      const rawBody = Buffer.isBuffer(req.rawBody) ? req.rawBody.toString("utf8") : JSON.stringify(req.body || {});
      const headers = Object.fromEntries(Object.entries(req.headers).map(([key, value]) => [key.toLowerCase(), Array.isArray(value) ? value[0] : value]));
      const callback = rechargeGateway.parseWechatNotification({ body: rawBody, headers });
      const order = await store.getPaymentOrderByOutTradeNo(callback.outTradeNo);
      if (!order) throw Object.assign(new Error("支付订单不存在"), { statusCode: 404, code: "PAYMENT_ORDER_NOT_FOUND" });
      if (callback.success) {
        const result = await store.completePaymentOrder({
          paymentOrderId: order.id,
          providerTradeNo: callback.providerTradeNo,
          amountCents: callback.amountCents,
        });
        await store.addAudit({
          action: "workspace.recharge_paid",
          actorUserId: null,
          targetType: "payment_order",
          targetId: order.id,
          metadata: { channel: "wechat", duplicate: result.duplicate === true },
        });
      }
      res.status(200).json({ code: "SUCCESS", message: "成功" });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/payments/alipay/notify", async (req, res, next) => {
    try {
      const callback = rechargeGateway.parseAlipayNotification(req.body || {});
      const order = await store.getPaymentOrderByOutTradeNo(callback.outTradeNo);
      if (!order) throw Object.assign(new Error("支付订单不存在"), { statusCode: 404, code: "PAYMENT_ORDER_NOT_FOUND" });
      if (callback.success) {
        const result = await store.completePaymentOrder({
          paymentOrderId: order.id,
          providerTradeNo: callback.providerTradeNo,
          amountCents: callback.amountCents,
        });
        await store.addAudit({
          action: "workspace.recharge_paid",
          actorUserId: null,
          targetType: "payment_order",
          targetId: order.id,
          metadata: { channel: "alipay", duplicate: result.duplicate === true },
        });
      }
      res.type("text/plain").send("success");
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/images/generate", requireUser, chatLimiter, mutationLimiter, async (req, res, next) => {
    let activeReserved = false;
    let createdJob = null;
    try {
      if (!imageEnabled) {
        throw Object.assign(new Error("图片生成能力尚未配置，请联系管理员"), { statusCode: 503, code: "LIBIMAGE_NOT_CONFIGURED" });
      }
      if (activeImageUsers.has(req.workspace.user.id)) {
        throw Object.assign(new Error("已有图片正在生成，请完成后再发起新任务"), { statusCode: 409, code: "IMAGE_JOB_ALREADY_RUNNING" });
      }
      if (activeImageJobs.size >= numberFromEnv("IMAGE_GENERATION_CONCURRENCY_LIMIT", 2)) {
        throw Object.assign(new Error("当前图片任务较多，请稍后再试"), { statusCode: 429, code: "IMAGE_CAPACITY_REACHED" });
      }
      activeImageUsers.add(req.workspace.user.id);
      activeReserved = true;

      const prompt = cleanText(req.body?.prompt, 2_000);
      if (!prompt) throw Object.assign(new Error("请填写图片生成要求"), { statusCode: 400, code: "IMAGE_PROMPT_REQUIRED" });
      if (containsRestrictedBusinessContent(prompt)) {
        throw Object.assign(new Error("该内容不在员工工作台的图片生成范围内"), { statusCode: 403, code: "IMAGE_SCOPE_DENIED" });
      }
      const model = cleanText(req.body?.model, 40);
      const selectedModel = IMAGE_MODELS[model];
      if (!selectedModel) throw Object.assign(new Error("请选择有效的图片模型"), { statusCode: 400, code: "IMAGE_MODEL_INVALID" });
      if (!libTvImageEnabled) {
        throw Object.assign(new Error("图片生成能力尚未配置，请联系管理员"), { statusCode: 503, code: "LIBIMAGE_NOT_CONFIGURED" });
      }
      const selectedImageProvider = imageProvider;
      const mode = cleanText(req.body?.mode, 40) || "text-to-image";
      if (!IMAGE_MODES[mode]) {
        throw Object.assign(new Error("请选择有效的图片生成方式"), { statusCode: 400, code: "IMAGE_MODE_INVALID" });
      }
      const attachmentIds = [...new Set((Array.isArray(req.body?.attachmentIds) ? req.body.attachmentIds : [])
        .map((id) => cleanText(id, 80))
        .filter(Boolean))];
      if (attachmentIds.length > 3) {
        throw Object.assign(new Error("一次最多添加 3 张参考图"), { statusCode: 400, code: "IMAGE_REFERENCE_LIMIT" });
      }
      if (mode === "image-to-image" && attachmentIds.length === 0) {
        throw Object.assign(new Error("图生图至少需要 1 张参考图"), { statusCode: 400, code: "IMAGE_REFERENCE_REQUIRED" });
      }
      if (mode === "text-to-image" && attachmentIds.length > 0) {
        throw Object.assign(new Error("文生图不能带参考图，请切换为图生图"), { statusCode: 400, code: "IMAGE_REFERENCE_NOT_ALLOWED" });
      }
      const referenceFiles = [];
      for (const fileId of attachmentIds) {
        const file = await store.getFile(fileId);
        const extension = extname(file?.originalName || "").toLowerCase();
        if (!file
          || file.ownerUserId !== req.workspace.user.id
          || file.status !== "ready"
          || file.workflowStatus !== "ordinary_chat_attachment"
          || file.chatAttachmentUsed
          || !LOVART_IMAGE_MIME_BY_EXTENSION.has(extension)) {
          throw new AuthError("参考图不存在、已使用或无权访问", "IMAGE_REFERENCE_FORBIDDEN", 404);
        }
        referenceFiles.push(file);
      }

      await store.ensureCreditAccount(req.workspace.user.id, imageInitialCredits);
      let conversation = req.body?.conversationId ? await store.getConversation(req.body.conversationId) : null;
      if (conversation && conversation.ownerUserId !== req.workspace.user.id) {
        throw new AuthError("无权使用该对话", "CONVERSATION_FORBIDDEN", 403);
      }
      if (!conversation) {
        await ensureConversationCapacity(req.workspace.user.id);
        conversation = await store.createConversation({
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          title: prompt.slice(0, 36),
          module: "视觉作图",
        });
      }
      const priorMessages = await store.listMessages(conversation.id);
      if (priorMessages.length >= 100) {
        throw Object.assign(new Error("当前对话已达 100 条，请新建对话继续"), { statusCode: 409, code: "MESSAGE_LIMIT" });
      }
      const imageJobPrompt = resolveImageJobPrompt(prompt, priorMessages);
      const charged = await store.createImageJobWithCredit({
        ownerUserId: req.workspace.user.id,
        departmentId: req.workspace.user.departmentId || null,
        conversationId: conversation.id,
        prompt: imageJobPrompt,
        model,
        modelLabel: selectedModel.label,
        mode,
        provider: selectedImageProvider,
        attachmentIds,
      }, { initialCredits: imageInitialCredits, cost: imageCreditCost });
      createdJob = charged.job;
      const userMessage = await store.addMessage({
        conversationId: conversation.id,
        ownerUserId: req.workspace.user.id,
        role: "user",
        content: prompt,
        attachmentIds,
        attachments: referenceFiles.map(serializeChatAttachment),
        module: "视觉作图",
        imageModel: model,
        imageModelLabel: selectedModel.label,
        imageMode: mode,
        imageProvider: selectedImageProvider,
      });
      await store.updateImageJob(createdJob.id, { userMessageId: userMessage.id });
      for (const file of referenceFiles) {
        await store.updateFile(file.id, {
          chatAttachmentUsed: true,
          conversationId: conversation.id,
          chatMessageId: userMessage.id,
          imageJobId: createdJob.id,
        });
      }
      activeImageJobs.add(createdJob.id);
      res.status(202).json({
        ok: true,
        conversationId: conversation.id,
        job: serializeImageJob(createdJob, [], charged.balance),
        balance: charged.balance,
      });
      if (!lovartRemoteEnabled) {
        queueMicrotask(() => { void processImageJob(createdJob.id, referenceFiles); });
      }
    } catch (error) {
      if (createdJob) await store.failImageJobAndRefund(createdJob.id, {
        errorCode: error?.code || "IMAGE_JOB_SETUP_FAILED",
        errorMessage: safeError(error).message,
      }).catch(() => {});
      if (activeReserved) activeImageUsers.delete(req.workspace.user.id);
      if (createdJob) activeImageJobs.delete(createdJob.id);
      next(error);
    }
  });

  app.get("/workspace-api/images/jobs/:id", requireUser, async (req, res, next) => {
    try {
      const job = await store.getImageJob(req.params.id);
      if (!job || (job.ownerUserId !== req.workspace.user.id && req.workspace.user.role !== "admin")) {
        throw new AuthError("图片任务不存在或无权查看", "IMAGE_JOB_FORBIDDEN", 404);
      }
      const files = [];
      for (const fileId of Array.isArray(job.artifactIds) ? job.artifactIds : []) {
        const file = await store.getFile(fileId);
        if (file) files.push(file);
      }
      const account = await store.ensureCreditAccount(job.ownerUserId, imageInitialCredits);
      const message = job.messageId ? await store.get("messages", job.messageId) : null;
      res.json({ ok: true, job: serializeImageJob(job, files, account.balance), message, balance: account.balance });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/internal/lovart/jobs/claim", requireLovartExecutor, async (req, res, next) => {
    try {
      const executorId = cleanText(req.body?.executorId, 80) || "manager-office";
      const job = await store.claimQueuedImageJob(executorId);
      if (!job) return res.status(204).end();
      const references = [];
      for (const fileId of Array.isArray(job.attachmentIds) ? job.attachmentIds : []) {
        const file = await store.getFile(fileId);
        if (!file || file.ownerUserId !== job.ownerUserId) continue;
        references.push({
          id: file.id,
          name: file.originalName,
          mimeType: file.mimeType,
          url: `/workspace-api/internal/lovart/jobs/${encodeURIComponent(job.id)}/references/${encodeURIComponent(file.id)}`,
        });
      }
      res.json({
        ok: true,
        job: {
          id: job.id,
          prompt: job.prompt,
          model: job.model,
          modelLabel: job.modelLabel,
          mode: job.mode || "text-to-image",
          provider: job.provider || imageProvider,
          references,
        },
      });
    } catch (error) { next(error); }
  });

  app.get(
    "/workspace-api/internal/lovart/jobs/:jobId/references/:fileId",
    requireLovartExecutor,
    async (req, res, next) => {
      try {
        const job = await store.getImageJob(req.params.jobId);
        const file = await store.getFile(req.params.fileId);
        if (!job
          || !file
          || job.status !== "running"
          || !Array.isArray(job.attachmentIds)
          || !job.attachmentIds.includes(file.id)
          || file.ownerUserId !== job.ownerUserId) {
          throw new AuthError("参考图不存在", "NOT_FOUND", 404);
        }
        const canonicalPath = await resolveStoredUpload(file);
        res.type(file.mimeType || "application/octet-stream");
        res.set("content-disposition", `attachment; filename*=UTF-8''${encodeURIComponent(file.originalName || "reference")}`);
        res.sendFile(canonicalPath);
      } catch (error) { next(error); }
    },
  );

  app.post(
    "/workspace-api/internal/lovart/jobs/:jobId/complete",
    requireLovartExecutor,
    lovartExecutorUpload.array("images", 4),
    async (req, res, next) => {
      let reservation;
      const temporaryPaths = (Array.isArray(req.files) ? req.files : []).map((file) => file.path);
      try {
        const job = await store.getImageJob(req.params.jobId);
        if (!job) throw new AuthError("图片任务不存在", "NOT_FOUND", 404);
        if (job.status === "done") {
          return res.json({ ok: true, jobId: job.id, alreadyCompleted: true });
        }
        if (job.status !== "running") throw new AuthError("图片任务不存在", "NOT_FOUND", 404);
        if (!temporaryPaths.length) {
          throw Object.assign(new Error("Lovart 没有返回可用图片"), { statusCode: 422, code: "LOVART_NO_IMAGE" });
        }
        reservation = await reserveStorageCapacity(job.ownerUserId, 50 * 1024 * 1024);
        const outputDir = join(artifactDir, "image-jobs", job.id);
        await mkdir(outputDir, { recursive: true, mode: 0o700 });
        const artifacts = [];
        for (const [index, file] of req.files.entries()) {
          const extension = extname(file.originalname).toLowerCase();
          const targetPath = join(outputDir, `${String(index + 1).padStart(2, "0")}${extension}`);
          await writeFile(targetPath, await readFile(file.path), { mode: 0o600 });
          artifacts.push(await validateGeneratedImage(targetPath, outputDir, 25 * 1024 * 1024));
        }
        if (artifacts.reduce((total, artifact) => total + artifact.size, 0) > 50 * 1024 * 1024) {
          throw Object.assign(new Error("图片总大小超过限制"), { statusCode: 413, code: "LOVART_ARTIFACT_TOTAL_SIZE_EXCEEDED" });
        }
        await releaseImageReferenceCache(job.attachmentIds, "job_completed");
        await finalizeImageJob(job, {
          threadId: cleanText(req.body?.threadId, 200) || null,
          canvasUrl: /^https:\/\//iu.test(String(req.body?.canvasUrl || "")) ? String(req.body.canvasUrl) : null,
          artifacts,
        }, Array.isArray(job.attachmentIds) ? job.attachmentIds.length : 0);
        activeImageUsers.delete(job.ownerUserId);
        activeImageJobs.delete(job.id);
        res.json({ ok: true, jobId: job.id });
      } catch (error) { next(error); }
      finally {
        await releaseStorageReservation(reservation).catch(() => {});
        await Promise.all(temporaryPaths.map((filePath) => rm(filePath, { force: true }).catch(() => {})));
      }
    },
  );

  app.post("/workspace-api/internal/lovart/jobs/:jobId/fail", requireLovartExecutor, async (req, res, next) => {
    try {
      const job = await store.getImageJob(req.params.jobId);
      if (!job) throw new AuthError("图片任务不存在", "NOT_FOUND", 404);
      if (job.status === "done") {
        return res.json({ ok: true, refunded: false, alreadyCompleted: true });
      }
      if (job.status === "failed") {
        return res.json({ ok: true, refunded: job.creditRefunded === true, alreadyFailed: true });
      }
      await releaseImageReferenceCache(job.attachmentIds, "job_failed");
      const failed = await store.failImageJobAndRefund(job.id, {
        errorCode: cleanText(req.body?.code, 80) || "LOVART_EXECUTOR_FAILED",
        errorMessage: cleanText(req.body?.message, 300) || "图片生成失败，积分已自动退回",
      });
      activeImageUsers.delete(job.ownerUserId);
      activeImageJobs.delete(job.id);
      await store.addAudit({
        action: "workspace.image_generation_failed",
        actorUserId: job.ownerUserId,
        targetType: "image_job",
        targetId: job.id,
        metadata: { code: failed?.job?.errorCode || "LOVART_EXECUTOR_FAILED", creditRefunded: true, executor: "manager-office" },
      });
      res.json({ ok: true, refunded: failed?.job?.creditRefunded === true });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/chat", requireUser, chatLimiter, async (req, res, next) => {
    let activeConversationId = null;
    let harnessTask = null;
    let harnessSequence = 0;
    let harnessRequestKey = null;
    let recordHarnessPhase = null;
    let harnessPreExecutionApproved = false;
    let harnessTaskReplayed = false;
    let harnessDeliveryOnly = false;
    let executionResultCommitted = false;
    const harnessEventKeys = new Set();
    let harnessReviews = [];
    let publishChatProgress = null;
    let chatProgressRequestId = null;
    const processingStartedAt = Date.now();
    try {
      const clientRequestId = safeRequestId(req.body?.requestId);
      chatProgressRequestId = clientRequestId;
      const potentialHarnessRequestKey = `workspace:${clientRequestId}`;
      let prompt = cleanText(req.body?.prompt, 6_000);
      const taskPolicy = resolveWorkspaceTaskPolicy({
        taskMode: req.body?.taskMode,
        permissionMode: req.body?.permissionMode,
      }, req.workspace.user);
      const attachmentIds = [...new Set((Array.isArray(req.body?.attachmentIds) ? req.body.attachmentIds : [])
        .map((id) => cleanText(id, 80))
        .filter(Boolean))];
      if (attachmentIds.length > 3) {
        throw Object.assign(new Error("一次最多添加 3 个附件"), { statusCode: 400, code: "CHAT_ATTACHMENT_LIMIT" });
      }
      if (!prompt && attachmentIds.length) prompt = "请阅读附件并总结重点。";
      if (!prompt) throw Object.assign(new Error("请输入任务内容"), { statusCode: 400 });
      chatProgressHub.open(req.workspace.user.id, clientRequestId);
      publishChatProgress = (phase) => chatProgressHub.publish(
        req.workspace.user.id,
        clientRequestId,
        { phase },
      );
      publishChatProgress("RECEIVED");
      res.once("finish", () => {
        if (res.statusCode < 400) chatProgressHub.complete(req.workspace.user.id, clientRequestId);
      });
      const policies = await store.listModelPolicies({ key: "default" });
      const policy = policies[0] || { defaultModel: DEFAULT_TEXT_MODEL_ID, allowEmployeeSwitch: true, dailyUserLimit: 100 };
      const defaultModel = normalizeTextModelId(policy.defaultModel || policy.defaultMode);
      const requestedModel = TEXT_MODEL_IDS.has(req.body?.model) ? req.body.model : normalizeTextModelId(req.body?.mode || defaultModel);
      const knowledgeScope = KNOWLEDGE_SCOPES.has(req.body?.knowledgeScope) ? req.body.knowledgeScope : "智能匹配";
      const permittedModel = req.workspace.user.role === "admin" || policy.allowEmployeeSwitch ? requestedModel : defaultModel;
      const attachments = [];
      for (const fileId of attachmentIds) {
        const file = await store.getFile(fileId);
        const replayedAttachment = file?.chatAttachmentUsed === true
          && file?.harnessRequestKey === potentialHarnessRequestKey;
        if (!file
          || file.ownerUserId !== req.workspace.user.id
          || file.status !== "ready"
          || file.workflowStatus !== "ordinary_chat_attachment"
          || (file.chatAttachmentUsed && !replayedAttachment)) {
          throw new AuthError("附件不存在、已使用或无权访问", "CHAT_ATTACHMENT_FORBIDDEN", 404);
        }
        attachments.push(file);
      }
      const routePlan = resolvePrivateChatRoute(prompt, knowledgeScope);
      const {
        selectedBusinessModule,
        effectiveModule,
        effectiveIntent,
        routedScope,
        autoRouted,
      } = routePlan;
      publishChatProgress("UNDERSTANDING");
      const model = knowledgeScope === CRM_EXPERT_SCOPE || routedScope === CRM_EXPERT_SCOPE ? CRM_TEXT_MODEL_ID : permittedModel;
      const shouldLoadCrmReadonly = routedScope === CRM_EXPERT_SCOPE && effectiveIntent === "crm_query";
      const requestedCrmOutsideSalesExpert = routedScope !== CRM_EXPERT_SCOPE
        && chooseHermesIntent({ prompt, module: moduleFor(prompt) }) === "crm_query";
      const requestedKnowledgeInsideSalesExpert = knowledgeScope === CRM_EXPERT_SCOPE
        && isExplicitKnowledgeQuestion(prompt);
      const explicitKnowledgeQuestion = isExplicitKnowledgeQuestion(prompt);
      const shouldSearchKnowledge = KNOWLEDGE_ENABLED_SCOPES.has(routedScope)
        && !requestedCrmOutsideSalesExpert
        && !isAttachmentOnlyQuestion(prompt, attachments)
        && (explicitKnowledgeQuestion || ["课程课件", "内容与选题"].includes(routedScope));
      publishChatProgress("ROUTING");

      const clientReplayMessages = await store.listMessages({ ownerUserId: req.workspace.user.id, clientRequestId });
      const completedClientReplay = effectiveIntent === "courseware_creation"
        ? [...clientReplayMessages].reverse().find((item) => item.role === "assistant" && item.route === "courseware_clarification")
        : [...clientReplayMessages].reverse().find((item) => item.role === "assistant");
      if (completedClientReplay) {
        res.json({
          ok: true,
          conversationId: completedClientReplay.conversationId,
          message: completedClientReplay,
          route: completedClientReplay.route || model,
          module: effectiveModule,
          routing: { mode: autoRouted ? "automatic" : "selected", module: effectiveModule, scope: routedScope, intent: effectiveIntent },
          citations: completedClientReplay.citations || [],
          agent: { status: "completed", intent: effectiveIntent, toolCount: 0 },
          replayed: true,
        });
        return;
      }
      const existingClientUserMessage = clientReplayMessages.find((item) => item.role === "user") || null;

      const today = new Date().toISOString().slice(0, 10);
      const userAudit = await store.listAudit({ actorUserId: req.workspace.user.id });
      const usedToday = userAudit.filter((event) => event.action === "workspace.chat_completed" && event.createdAt?.startsWith(today)).length;
      if (usedToday >= Number(policy.dailyUserLimit || 100)) {
        throw Object.assign(new Error("今日任务额度已用完，请联系管理员"), { statusCode: 429, code: "DAILY_LIMIT_REACHED" });
      }

      let conversation = existingClientUserMessage
        ? await store.getConversation(existingClientUserMessage.conversationId)
        : req.body?.conversationId ? await store.getConversation(req.body.conversationId) : null;
      if (conversation && conversation.ownerUserId !== req.workspace.user.id) {
        throw new AuthError("无权使用该对话", "CONVERSATION_FORBIDDEN", 403);
      }
      if (!conversation) {
        await ensureConversationCapacity(req.workspace.user.id);
        conversation = await store.createConversation({
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          title: prompt.slice(0, 36),
          module: knowledgeScope,
        });
      }
      let coursewareRequestMessage = existingClientUserMessage;
      if (effectiveIntent === "courseware_creation" && !coursewareRequestMessage) {
        coursewareRequestMessage = await store.addMessage({
          conversationId: conversation.id,
          ownerUserId: req.workspace.user.id,
          role: "user",
          content: redactSensitive(prompt),
          clientRequestId,
          attachmentIds,
          attachments: attachments.map(serializeChatAttachment),
          harnessRequestKey: potentialHarnessRequestKey,
        });
        for (const file of attachments) {
          await store.updateFile(file.id, {
            chatAttachmentUsed: true,
            conversationId: conversation.id,
            chatMessageId: coursewareRequestMessage.id,
            harnessRequestKey: potentialHarnessRequestKey,
          });
        }
      }
      if (existingClientUserMessage && (activeConversationRequests.get(conversation.id) || 0) > 0) {
        throw Object.assign(new Error("本轮任务仍在处理中，工作台将自动重试"), { statusCode: 409, code: "CHAT_REQUEST_IN_PROGRESS" });
      }
      activeConversationId = conversation.id;
      activeConversationRequests.set(activeConversationId, (activeConversationRequests.get(activeConversationId) || 0) + 1);
      const taskRiskLevel = classifyWorkspaceRisk(prompt);
      const preTaskMessages = await store.listMessages(conversation.id);
      const agentPrompt = buildHermesTaskPrompt(
        expandKnowledgeCardSelectionPrompt(prompt, { history: preTaskMessages }),
        taskPolicy,
      );
      const taskHistorySnapshot = selectChatHistorySnapshot(preTaskMessages, {
        excludeHarnessRequestKey: potentialHarnessRequestKey,
        excludeClientRequestId: clientRequestId,
      });
      const taskAttachmentSnapshot = attachments
        .map((file) => ({
          id: String(file.id),
          sha256: String(file.sha256 || ""),
          mimeType: String(file.mimeType || ""),
          size: Number(file.size || 0),
        }))
        .sort((left, right) => left.id.localeCompare(right.id));
      if (effectiveIntent === "courseware_creation" && coursewareBriefNeedsClarification({
        prompt,
        history: taskHistorySnapshot,
        attachmentCount: taskAttachmentSnapshot.length,
      })) {
        const clarification = await store.addMessage({
          conversationId: conversation.id,
          ownerUserId: req.workspace.user.id,
          role: "assistant",
          clientRequestId,
          content: "可以做，但现在的信息不足以生成可验收的 PPT。请直接回复一行：主题｜受众或使用场景｜电子杂志风或瑞士风（不确定可写‘按内容推荐’）。未写页数时默认 8 页。",
          route: "courseware_clarification",
          model,
          module: knowledgeScope,
          citations: [],
          processingTrace: {
            status: "needs_input",
            capability: "courseware",
            skill: "guizang-ppt-skill",
          },
        });
        await store.updateConversation(conversation.id, { lastMessageAt: clarification.createdAt, module: knowledgeScope });
        await store.addAudit({
          action: "workspace.courseware_clarification_requested",
          actorUserId: req.workspace.user.id,
          targetType: "conversation",
          targetId: conversation.id,
          metadata: { skill: "guizang-ppt-skill", reason: "brief_incomplete" },
        });
        res.json({
          ok: true,
          conversationId: conversation.id,
          message: clarification,
          route: clarification.route,
          module: effectiveModule,
          routing: { mode: autoRouted ? "automatic" : "selected", module: effectiveModule, scope: routedScope, intent: effectiveIntent },
          citations: [],
          agent: { status: "needs_input", intent: effectiveIntent, toolCount: 0 },
        });
        return;
      }
      if ((agentHarness.enabled || agentHarness.required) && effectiveIntent === "courseware_creation") {
        harnessRequestKey = `workspace:${clientRequestId}`;
        const createdHarnessTask = await agentHarness.createTask({
          requestKey: harnessRequestKey,
          actorUserId: req.workspace.user.id,
          conversationId: conversation.id,
          capability: harnessCapabilityFor(effectiveModule),
          title: prompt.slice(0, 200),
          requestSha256: sha256Summary([
            agentHarness.tenantKey,
            req.workspace.user.id,
            conversation.id,
            effectiveModule,
            effectiveIntent,
            routedScope,
            model,
            prompt,
            JSON.stringify(taskAttachmentSnapshot),
            JSON.stringify(taskHistorySnapshot),
          ].join("\n")),
          riskLevel: taskRiskLevel,
          requiresReview: true,
          executorType: "hermes",
          modelId: model,
          metadata: {
            contract: "workspace-courseware.v1",
            entrypoint: "employee_workspace_chat",
            runtime: "hermes",
            skill: "guizang-ppt-skill",
            skill_mode: "static_pptx",
            task_mode: taskPolicy.taskMode,
            permission_mode: taskPolicy.permissionMode,
            knowledge_scope: routedScope,
            readonly: true,
            approval_policy: taskRiskLevel === "high" ? "pre_execution_and_result" : "result",
            input_snapshot: "workspace-courseware-input.v2",
            attachment_count: taskAttachmentSnapshot.length,
            history_message_count: taskHistorySnapshot.length,
          },
        });
        harnessTask = createdHarnessTask?.task || null;
        harnessTaskReplayed = createdHarnessTask?.replayed === true;
        if (!harnessTask) {
          throw Object.assign(new Error("任务中枢未返回有效任务"), { statusCode: 503, code: "AGENT_HARNESS_TASK_MISSING" });
        }
        if (createdHarnessTask?.replayed) {
          const replayMessages = await store.listMessages(conversation.id);
          let existingAnswer = [...replayMessages].reverse().find(
            (item) => item.role === "assistant"
              && item.harnessTaskId === harnessTask?.id
              && item.route !== "approval_required",
          );
          const hasExecutionLeaseClient = typeof agentHarness.recoverExecutionLease === "function"
            && typeof agentHarness.ackExecutionResultOutbox === "function";
          const deliveryAlreadyFinished = harnessTask?.current_phase === "FINISHED"
            || ["WAITING_REVIEW", "APPROVED"].includes(harnessTask?.status);
          if (existingAnswer && (!hasExecutionLeaseClient || deliveryAlreadyFinished)) {
            const publicTask = publicHarnessState(harnessTask);
            const replayAction = existingAnswer.coursewareAction || buildCoursewareAction({
              title: conversation.title,
              harnessTaskId: harnessTask.id,
              content: existingAnswer.content,
            });
            if (existingAnswer.harnessTaskStatus !== harnessTask.status
                || existingAnswer.harnessPublicState?.status !== publicTask.status) {
              existingAnswer = await store.updateMessage(existingAnswer.id, {
                harnessTaskStatus: harnessTask.status,
                harnessTaskVersion: harnessTask.version,
                harnessPublicState: publicTask,
                coursewareAction: replayAction,
              });
            } else if (!existingAnswer.coursewareAction) {
              existingAnswer = await store.updateMessage(existingAnswer.id, { coursewareAction: replayAction });
            }
            res.json({
              ok: true,
              conversationId: conversation.id,
              message: existingAnswer,
              route: existingAnswer.route,
              module: effectiveModule,
              routing: { mode: autoRouted ? "automatic" : "selected", module: effectiveModule, scope: routedScope, intent: effectiveIntent },
              citations: existingAnswer.citations || [],
              agent: { status: "completed", intent: effectiveIntent, toolCount: 0 },
              task: { id: harnessTask.id, ...publicTask },
              replayed: true,
            });
            return;
          }
          const refreshed = await agentHarness.getTask(harnessTask.id);
          harnessTask = refreshed?.task || harnessTask;
          const replayEvents = Array.isArray(refreshed?.events) ? refreshed.events : [];
          harnessReviews = Array.isArray(refreshed?.reviews) ? refreshed.reviews : [];
          harnessSequence = Math.max(
            0,
            ...replayEvents.map((item) => Number(item.sequence) || 0),
          );
          for (const event of replayEvents) {
            const eventKey = String(event.event_key || event.eventKey || "").trim();
            if (eventKey) harnessEventKeys.add(eventKey);
          }
        }
        if (harnessTask.status === "WAITING_REVIEW" && harnessTask.approval_stage === "pre_execution") {
          const publicTask = publicHarnessState(harnessTask);
          const approvalMessages = await store.listMessages(conversation.id);
          let approvalUserMessage = approvalMessages.find(
            (item) => item.role === "user" && item.harnessRequestKey === harnessRequestKey,
          );
          if (!approvalUserMessage) {
            approvalUserMessage = coursewareRequestMessage || await store.addMessage({
              conversationId: conversation.id,
              ownerUserId: req.workspace.user.id,
              role: "user",
              content: redactSensitive(prompt),
              clientRequestId,
              attachmentIds,
              attachments: attachments.map(serializeChatAttachment),
              harnessRequestKey,
            });
            for (const file of attachments) {
              await store.updateFile(file.id, {
                chatAttachmentUsed: true,
                conversationId: conversation.id,
                chatMessageId: approvalUserMessage.id,
                harnessRequestKey,
              });
            }
          }
          let approvalMessage = approvalMessages.find(
            (item) => item.role === "assistant"
              && item.harnessTaskId === harnessTask.id
              && item.route === "approval_required",
          );
          if (!approvalMessage) {
            approvalMessage = await store.addMessage({
              conversationId: conversation.id,
              ownerUserId: req.workspace.user.id,
              role: "assistant",
              content: "该课件任务需要老板审批。审批通过前不会调用模型、知识库或执行工具。",
              route: "approval_required",
              model,
              module: knowledgeScope,
              citations: [],
              harnessTaskId: harnessTask.id,
              harnessRequestKey,
              harnessTaskStatus: harnessTask.status,
              harnessTaskVersion: harnessTask.version,
              harnessPublicState: publicTask,
            });
          }
          await store.updateConversation(conversation.id, {
            lastMessageAt: approvalMessage.createdAt,
            module: knowledgeScope,
          });
          res.status(202).json({
            ok: true,
            conversationId: conversation.id,
            message: approvalMessage,
            route: "approval_required",
            module: effectiveModule,
            routing: { mode: autoRouted ? "automatic" : "selected", module: effectiveModule, scope: routedScope, intent: effectiveIntent },
            citations: [],
            agent: { status: "waiting_review", intent: effectiveIntent, toolCount: 0 },
            task: { id: harnessTask.id, version: harnessTask.version, approvalStage: "pre_execution", ...publicTask },
            ...(createdHarnessTask?.replayed ? { replayed: true } : {}),
          });
          return;
        }
        harnessPreExecutionApproved = harnessReviews.some(
          (review) => (review.review_stage || review.reviewStage) === "pre_execution"
            && review.decision === "approve",
        );
        if (harnessTask.risk_level === "high"
          && createdHarnessTask?.replayed
          && !harnessPreExecutionApproved) {
          throw Object.assign(new Error("高风险课件任务尚未完成执行前审批"), {
            statusCode: 409,
            code: "COURSEWARE_PRE_EXECUTION_APPROVAL_REQUIRED",
          });
        }
        harnessDeliveryOnly = Boolean(
          harnessTask.public_result
            || harnessTask.publicResult
            || harnessTask.result_outbox
            || harnessTask.resultOutbox,
        );
        if (!['QUEUED', 'RUNNING'].includes(harnessTask.status) && !harnessDeliveryOnly) {
          throw Object.assign(new Error("当前课件任务不能继续执行"), { statusCode: 409, code: "COURSEWARE_TASK_NOT_RUNNABLE" });
        }
        recordHarnessPhase = async (phase, summary, extra = {}) => {
          const eventKey = `${harnessRequestKey}:${phase.toLowerCase()}${extra.eventKeySuffix ? `:${extra.eventKeySuffix}` : ""}`;
          if (harnessEventKeys.has(eventKey)) return { replayed: true, eventKey };
          harnessSequence += 1;
          const result = await agentHarness.recordEvent(harnessTask.id, {
            eventKey,
            eventType: extra.eventType || "phase",
            phase,
            sequence: harnessSequence,
            summarySha256: sha256Summary(summary),
            executorType: "hermes",
            modelId: model,
            ...extra,
          });
          harnessTask = result?.task || harnessTask;
          harnessEventKeys.add(eventKey);
          const publicPhase = {
            CLASSIFIED: "UNDERSTANDING",
            PLANNED: "ROUTING",
            EXECUTING: "EXECUTING",
            CHECKPOINTED: "EXECUTING",
            VERIFYING: "VERIFYING",
            FINISHED: "COMPLETED",
            RUN_FAILED: "FAILED",
          }[phase];
          if (publicPhase) publishChatProgress?.(publicPhase);
          return result;
        };
        if (!harnessDeliveryOnly) {
          await recordHarnessPhase("CLASSIFIED", `courseware:${effectiveModule}:${effectiveIntent}`);
          await recordHarnessPhase("PLANNED", "courseware-plan:retrieve-draft-verify-export-review");
          await recordHarnessPhase("EXECUTING", `courseware-execute:${model}`);
        }
      }
      const priorMessages = preTaskMessages;
      if (priorMessages.length >= 100) {
        throw Object.assign(new Error("当前对话已达 100 条，请新建对话继续"), { statusCode: 409, code: "MESSAGE_LIMIT" });
      }
      const historyLines = renderChatHistoryLines(selectChatHistorySnapshot(priorMessages, {
        excludeHarnessRequestKey: harnessRequestKey || "",
        excludeHarnessTaskId: harnessTask?.id || "",
        excludeClientRequestId: clientRequestId,
      }));
      const existingRequestMessage = harnessRequestKey
        ? priorMessages.find((item) => item.role === "user" && item.harnessRequestKey === harnessRequestKey)
        : null;
      const userMessage = existingRequestMessage || coursewareRequestMessage || existingClientUserMessage || await store.addMessage({
        conversationId: conversation.id,
        ownerUserId: req.workspace.user.id,
        role: "user",
        content: redactSensitive(prompt),
        clientRequestId,
        attachmentIds,
        attachments: attachments.map(serializeChatAttachment),
        ...(harnessRequestKey ? { harnessRequestKey } : {}),
      });
      for (const file of attachments) {
        if (file.chatAttachmentUsed && file.harnessRequestKey === harnessRequestKey) continue;
        await store.updateFile(file.id, {
          chatAttachmentUsed: true,
          conversationId: conversation.id,
          chatMessageId: userMessage.id,
          ...(harnessRequestKey ? { harnessRequestKey } : {}),
        });
      }

      if (shouldSearchKnowledge || shouldLoadCrmReadonly || attachments.length) {
        publishChatProgress("SOURCES");
      }
      const [documents, chunks, externalDocuments] = shouldSearchKnowledge
        ? await Promise.all([
          store.listKnowledgeDocuments({ status: "published" }),
          store.listKnowledgeChunks(),
          externalKnowledge.listDocuments().catch(() => []),
        ])
        : [[], [], []];
      const scoped = filterKnowledgeScope([
        ...buildKnowledgeDocuments(documents.filter((document) => !document.externalized), chunks),
        ...externalDocuments,
      ], routedScope, req.workspace.user);
      // Data domains are intentionally isolated by the selected expert:
      // Sales & CRM can call CRM read-only and never loads company knowledge.
      // All other experts can search company knowledge and never call CRM.
      const hits = shouldSearchKnowledge
        ? searchKnowledge(prompt.slice(0, 1_500), scoped, req.workspace.user, { limit: 5 })
        : [];
      let citations = hits.map((hit, index) => ({
        id: hit.documentId,
        title: hit.title,
        index: index + 1,
        source: hit.source,
        sourcePath: hit.sourcePath,
        sourceUpdatedAt: hit.sourceUpdatedAt,
      }));
      const contextSections = [];
      if (historyLines.length) contextSections.push(`[本次对话历史]\n${historyLines.join("\n")}`);
      if (attachments.length) {
        const attachmentContext = attachments.map((file, index) => {
          if (file.extractedText) return `[附件${index + 1}] ${file.originalName}\n${file.extractedText}`;
          return `[附件${index + 1}] ${file.originalName}\n这是员工本次上传的图片。当前只能确认文件已附加；涉及图片画面细节时必须明确说明无法可靠识别，并请员工补充文字描述。`;
        }).join("\n\n");
        contextSections.push(`[本次上传附件]\n${attachmentContext}`);
      }
      const knowledgeContext = hits.map((hit, index) => `[资料${index + 1}] ${hit.title}\n${hit.excerpt}`).join("\n\n");
      if (knowledgeContext) contextSections.push(knowledgeContext);
      if (shouldLoadCrmReadonly) {
        if (!crmReadonly.enabled) {
          throw Object.assign(new Error("CRM只读查询尚未连接"), { statusCode: 503, code: "CRM_READONLY_NOT_CONFIGURED" });
        }
        let readonlyContext = null;
        if (crmReadonly.enabled) {
          try {
            readonlyContext = await crmReadonly.queryContext({
              prompt,
              user: req.workspace.user,
              limit: req.workspace.user.role === "admin" ? 10 : req.workspace.user.role === "manager" ? 8 : 3,
              conversationContext: historyLines.join("\n"),
            });
          } catch (error) {
            if (error instanceof CrmReadonlyError) throw error;
            throw Object.assign(new Error("CRM只读查询暂时不可用，请稍后重试"), { statusCode: 503, code: "CRM_READONLY_UNAVAILABLE" });
          }
        }
        if (readonlyContext?.context) contextSections.push(`[CRM在线只读数据]\n${readonlyContext.context}`);
        const crmContext = await crm.buildAiContextSummary(req.workspace.user, { limit: 20 });
        if (crmContext) contextSections.push(`[本人授权销售数据]\n${crmContext}`);
      }
      if (routedScope === "内容与选题") {
        const topicHistory = (await store.listContentTopics({ status: "active" }))
          .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
          .slice(0, 30)
          .map((topic) => `- ${topic.title}${topic.corePoint ? `｜${topic.corePoint}` : ""}${topic.audience ? `｜受众：${topic.audience}` : ""}`)
          .join("\n");
        if (topicHistory) contextSections.push(`[公司近期内容选题，仅用于避免重复]\n${topicHistory}`);
      }
      const context = contextSections.join("\n\n");
      if (recordHarnessPhase) {
        await recordHarnessPhase(
          "CHECKPOINTED",
          `courseware-sources:${citations.map((item) => item.id).sort().join(",") || "none"}`,
          {
            eventType: "checkpoint",
            eventKeySuffix: "sources",
            knowledgeRefs: citations.map((item) => `knowledge:${item.id}`),
            metadata: { checkpoint: "sources_ready", citation_count: citations.length },
          },
        );
      }
      const agentRequestId = `workspace-${conversation.id}-${clientRequestId}`;
      publishChatProgress("EXECUTING");
      const generated = requestedCrmOutsideSalesExpert
        ? {
          answer: "**当前助手未连接 CRM。**\n\n请切换到“销售与 CRM”专家后重新查询；该专家只读取 CRM 在线数据，不会调用公司知识库或写回 CRM。",
          route: "scope_guard",
          upgraded: false,
          module: effectiveModule,
          intent: "crm_scope_required",
          profile: "",
          toolTrace: [],
        }
        : requestedKnowledgeInsideSalesExpert
          ? {
            answer: "**当前专家不读取公司知识库。**\n\n请切换到“课程课件”“内容与选题”或“通用助手”后重新提问。",
            route: "scope_guard",
            upgraded: false,
            module: effectiveModule,
            intent: "knowledge_scope_required",
            profile: "",
            toolTrace: [],
          }
          : await generate({
          model,
          prompt: agentPrompt,
          context,
          citations,
          hasFile: attachments.length > 0,
          module: effectiveModule === "通用办公" ? undefined : effectiveModule,
          user: req.workspace.user,
          requestId: agentRequestId,
          requireCitations: explicitKnowledgeQuestion && citations.length > 0,
          orchestrationConstraints: { riskLevel: taskRiskLevel },
          orchestrationRequestKey: harnessRequestKey || agentRequestId,
          langGraphAdapter,
          orchestrationContext: {
            ...(harnessPreExecutionApproved && harnessRequestKey ? {
              approval: { status: "approved", requestKey: harnessRequestKey },
            } : {}),
            ...(recordHarnessPhase ? {
              recordHarnessCheckpoint: async (checkpoint) => recordHarnessPhase(
                "CHECKPOINTED",
                `langgraph:${checkpoint.checkpoint}`,
                {
                  eventType: "checkpoint",
                  eventKeySuffix: `langgraph:${checkpoint.checkpoint}`,
                  metadata: checkpoint.metadata || {},
                },
              ),
            } : {}),
            ...(harnessTask
              && typeof agentHarness.acquireExecutionLease === "function"
              && typeof agentHarness.heartbeatExecutionLease === "function"
              && typeof agentHarness.recoverExecutionLease === "function"
              && typeof agentHarness.completeExecutionLease === "function"
              ? {
                executionLeaseClient: agentHarness,
                executionLease: {
                  taskId: harnessTask.id,
                  taskVersion: harnessTask.version,
                  checkpointSequence: harnessSequence,
                  recover: harnessTaskReplayed || harnessDeliveryOnly,
                },
              }
              : {}),
          },
        });
      if (generated.executionDurability
          && typeof executionRecoveryHooks.afterOutboxCommitted === "function") {
        executionResultCommitted = true;
        await executionRecoveryHooks.afterOutboxCommitted({ ...generated.executionDurability });
      }
      if (generated.executionDurability) executionResultCommitted = true;
      if (effectiveIntent === "courseware_creation") publishChatProgress("ARTIFACTS");
      publishChatProgress("VERIFYING");
      if (recordHarnessPhase) {
        await recordHarnessPhase(
          "VERIFYING",
          `courseware-answer:${sha256Summary(generated.answer)}`,
          {
            externalRunId: agentRequestId,
            executorProfile: generated.profile || null,
            metadata: { check: "format_citations_permissions" },
          },
        );
      }
      const aligned = alignAnswerCitations(generated.answer, citations);
      generated.answer = aligned.answer;
      citations = aligned.citations;
      const processingTrace = buildProcessingTrace({
        effectiveModule,
        autoRouted,
        citations,
        attachments,
        crmReadonlyQueried: shouldLoadCrmReadonly,
        shouldSearchKnowledge,
        durationMs: Date.now() - processingStartedAt,
      });
      if (recordHarnessPhase) {
        await recordHarnessPhase(
          "CHECKPOINTED",
          `courseware-draft:${sha256Summary(generated.answer)}`,
          {
            eventType: "checkpoint",
            eventKeySuffix: "draft",
            knowledgeRefs: citations.map((item) => `knowledge:${item.id}`),
            externalRunId: agentRequestId,
            executorProfile: generated.profile || null,
            metadata: { checkpoint: "draft_ready", next_action: "export_courseware" },
          },
        );
      }
      const harnessPublicTask = harnessTask ? publicHarnessState(harnessTask) : null;
      const knowledgeCardMeta = knowledgeCardMetaForPrompt(prompt, { history: preTaskMessages });
      let knowledgeCardArtifact = null;
      if (knowledgeCardMeta.knowledgeCardImage) {
        const artifactFiles = await createKnowledgeCardArtifacts({
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          content: generated.answer,
        });
        knowledgeCardArtifact = serializeGeneratedImage(artifactFiles.pngFile);
      }
      const storedAssistant = await store.addAssistantMessageOnceByHarnessTask({
        conversationId: conversation.id,
        ownerUserId: req.workspace.user.id,
        role: "assistant",
        clientRequestId,
        content: generated.answer,
        route: generated.route,
        model: generated.model || model,
        module: knowledgeScope,
        ...knowledgeCardMeta,
        ...(knowledgeCardArtifact ? { artifacts: [knowledgeCardArtifact] } : {}),
        citations,
        processingTrace,
        ...(effectiveIntent === "courseware_creation" ? {
          coursewareAction: buildCoursewareAction({
            title: conversation.title,
            harnessTaskId: harnessTask?.id,
            content: generated.answer,
          }),
        } : {}),
        ...(harnessTask ? {
          harnessTaskId: harnessTask.id,
          harnessRequestKey,
          harnessTaskStatus: harnessTask.status,
          harnessTaskVersion: harnessTask.version,
          harnessNextSequence: harnessSequence + 1,
          harnessPublicState: harnessPublicTask,
          ...(generated.executionDurability?.outboxId ? {
            harnessResultOutboxId: generated.executionDurability.outboxId,
          } : {}),
        } : {}),
      });
      let message = storedAssistant.message;
      if (generated.executionDurability
          && typeof executionRecoveryHooks.afterLocalMessagePersisted === "function") {
        await executionRecoveryHooks.afterLocalMessagePersisted({
          message: { ...message },
          durability: { ...generated.executionDurability },
        });
      }
      if (generated.executionDurability
          && !generated.executionDurability.outboxAcknowledged
          && typeof agentHarness.ackExecutionResultOutbox === "function") {
        await agentHarness.ackExecutionResultOutbox(
          generated.executionDurability.taskId,
          generated.executionDurability.outboxId,
          {
            ackKey: `workspace:result-ack:${generated.executionDurability.outboxId}`,
            ackToken: generated.executionDurability.ackToken,
            outboxVersion: generated.executionDurability.outboxVersion,
            localMessageId: message.id,
          },
        );
      }
      if (generated.executionDurability
          && typeof executionRecoveryHooks.afterOutboxAcknowledged === "function") {
        await executionRecoveryHooks.afterOutboxAcknowledged({
          message: { ...message },
          durability: { ...generated.executionDurability },
        });
      }
      // The model draft is only a checkpoint for courseware. The durable task remains
      // runnable until the PPTX and speaker-notes artifacts have been rendered and
      // verified by /courseware/export.
      if (harnessTask && (message.harnessTaskStatus !== harnessTask.status
          || message.harnessPublicState?.status !== harnessTask.status)) {
        message = await store.updateMessage(message.id, {
          harnessTaskStatus: harnessTask.status,
          harnessTaskVersion: harnessTask.version,
          harnessNextSequence: harnessSequence + 1,
          harnessPublicState: publicHarnessState(harnessTask),
        });
      }
      await store.updateConversation(conversation.id, { lastMessageAt: message.createdAt, module: knowledgeScope });
      await store.addAudit({
        action: "workspace.chat_completed",
        actorUserId: req.workspace.user.id,
        targetType: "conversation",
        targetId: conversation.id,
        metadata: {
          model,
          route: generated.route,
          module: generated.module,
          requestedScope: knowledgeScope,
          routedScope,
          autoRouted,
          agentIntent: generated.intent,
          agentProfile: generated.profile,
          agentRequestId,
          toolCount: generated.toolTrace?.length || 0,
          citationCount: citations.length,
          attachmentCount: attachments.length,
          crmReadonlyEnabled: crmReadonly.enabled,
          crmReadonlyQueried: shouldLoadCrmReadonly,
          processingDurationMs: processingTrace.durationMs,
          harnessTaskId: harnessTask?.id || null,
          harnessTaskStatus: harnessTask?.status || null,
          runtime: "hermes",
          taskMode: taskPolicy.taskMode,
          permissionMode: taskPolicy.permissionMode,
        },
      });
      const finalHarnessPublicTask = harnessTask ? publicHarnessState(harnessTask) : null;
      res.json({
        ok: true,
        conversationId: conversation.id,
        message,
        route: generated.route,
        module: generated.module,
        routing: { mode: autoRouted ? "automatic" : "selected", module: effectiveModule, scope: routedScope, intent: generated.intent },
        upgraded: generated.upgraded,
        citations,
        agent: { status: "completed", runtime: "hermes", intent: generated.intent, toolCount: generated.toolTrace?.length || 0 },
        taskPolicy: {
          version: taskPolicy.version,
          taskMode: taskPolicy.taskMode,
          permissionMode: taskPolicy.permissionMode,
        },
        ...(harnessTask ? { task: { id: harnessTask.id, ...finalHarnessPublicTask } } : {}),
      });
    } catch (error) {
      if (publishChatProgress) {
        chatProgressHub.fail(req.workspace.user.id, chatProgressRequestId);
      }
      if (harnessTask
          && recordHarnessPhase
          && !executionResultCommitted
          && !["APPROVED", "REJECTED", "FAILED", "CANCELLED", "WAITING_REVIEW"].includes(harnessTask.status)) {
        const errorCode = safeDiagnosticCode(error?.code, "COURSEWARE_EXECUTION_FAILED");
        if (errorCode === "LANGGRAPH_EXECUTION_ALREADY_CLAIMED") {
          next(error);
          return;
        }
        const retryableExecutionGap = errorCode === "LANGGRAPH_NOT_CONFIGURED"
          || [502, 503, 504].includes(Number(error?.statusCode));
        await recordHarnessPhase(
          retryableExecutionGap ? "RETRY_WAIT" : "RUN_FAILED",
          `courseware-${retryableExecutionGap ? "retry" : "failed"}:${errorCode}`,
          {
            eventType: retryableExecutionGap ? "status" : "failure",
            eventKeySuffix: errorCode.toLowerCase(),
            ...(retryableExecutionGap ? {
              metadata: {
                retryable: true,
                blocked_by: errorCode === "LANGGRAPH_NOT_CONFIGURED" ? "langgraph_adapter" : "model_gateway",
              },
            } : {
              errorCode,
              errorMessage: cleanText(error?.message, 300) || "课件任务执行失败",
            }),
          },
        ).catch((harnessError) => console.error("agent_harness_failure_event_failed", {
          code: safeDiagnosticCode(harnessError?.code, "AGENT_HARNESS_FAILURE_EVENT_FAILED"),
        }));
      }
      next(error);
    }
    finally {
      if (activeConversationId) {
        const remaining = (activeConversationRequests.get(activeConversationId) || 1) - 1;
        if (remaining > 0) activeConversationRequests.set(activeConversationId, remaining);
        else activeConversationRequests.delete(activeConversationId);
      }
    }
  });

  app.post("/workspace-api/document/export", requireUser, artifactLimiter, async (req, res, next) => {
    const writtenPaths = [];
    let storedFileId = null;
    try {
      if (activeArtifactUsers.has(req.workspace.user.id)) {
        throw Object.assign(new Error("已有文件正在生成，请等待完成后再试"), { statusCode: 409, code: "ARTIFACT_ALREADY_RUNNING" });
      }
      if (activeArtifactUsers.size >= numberFromEnv("ARTIFACT_CONCURRENCY_LIMIT", 3)) {
        throw Object.assign(new Error("当前文件任务较多，请稍后再试"), { statusCode: 429, code: "ARTIFACT_CAPACITY_REACHED" });
      }
      activeArtifactUsers.add(req.workspace.user.id);
      req.artifactReservation = await reserveStorageCapacity(req.workspace.user.id, 10 * 1024 * 1024);
      const rawContent = String(req.body?.content || "").replace(/\r\n?/gu, "\n").trim();
      const buffer = await buildLessonPlanDocx({
        title: cleanText(req.body?.title, 80),
        content: rawContent,
        author: req.workspace.user.displayName || req.workspace.user.name || "创创",
      });
      const outputPath = join(artifactDir, `${randomUUID()}.docx`);
      await writeFile(outputPath, buffer, { mode: 0o600 });
      writtenPaths.push(outputPath);
      const file = await store.transaction((draft) => {
        const timestamp = new Date().toISOString();
        const createdFile = {
          id: randomUUID(),
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          originalName: buffer.filename,
          storagePath: outputPath,
          mimeType: buffer.contentType,
          size: buffer.length,
          sha256: createHash("sha256").update(buffer).digest("hex"),
          scope: "personal",
          space: "课程课件",
          status: "ready",
          artifactType: "courseware_docx",
          createdAt: timestamp,
          updatedAt: timestamp,
        };
        draft.files.push(createdFile);
        draft.auditEvents.push({
          id: randomUUID(),
          action: "document.exported",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: createdFile.id,
          metadata: { artifactType: "courseware_docx", contentLength: rawContent.length },
          createdAt: timestamp,
          updatedAt: timestamp,
        });
        return createdFile;
      });
      storedFileId = file.id;
      res.status(201).json({ ok: true, format: "docx", file: serializeFile(file) });
    } catch (error) {
      if (storedFileId) await store.delete("files", storedFileId).catch(() => {});
      for (const outputPath of writtenPaths) await rm(outputPath, { force: true }).catch(() => {});
      if (["TypeError", "RangeError"].includes(error?.name)) error.statusCode ||= 400;
      next(error);
    } finally {
      activeArtifactUsers.delete(req.workspace.user.id);
      await releaseStorageReservation(req.artifactReservation);
    }
  });

  app.post("/workspace-api/courseware/export", requireUser, artifactLimiter, async (req, res, next) => {
    const writtenPaths = [];
    const storedFileIds = [];
    let exportHarnessTask = null;
    let exportHarnessSequence = 0;
    let exportHarnessEventBase = null;
    let recordExportPhase = null;
    let artifactCheckpointMayExist = false;
    let requestedHarnessTaskId = null;
    try {
      if (activeArtifactUsers.has(req.workspace.user.id)) {
        throw Object.assign(new Error("已有课件正在生成，请等待完成后再试"), { statusCode: 409, code: "ARTIFACT_ALREADY_RUNNING" });
      }
      if (activeArtifactUsers.size >= numberFromEnv("ARTIFACT_CONCURRENCY_LIMIT", 3)) {
        throw Object.assign(new Error("当前课件任务较多，请稍后再试"), { statusCode: 429, code: "ARTIFACT_CAPACITY_REACHED" });
      }
      activeArtifactUsers.add(req.workspace.user.id);
      const artifactReservation = await reserveStorageCapacity(req.workspace.user.id, 10 * 1024 * 1024);
      req.artifactReservation = artifactReservation;
      const spec = buildCoursewareSpec({
        title: req.body?.title,
        content: req.body?.content,
        author: req.workspace.user.displayName || req.workspace.user.name || "创创",
      });
      const exportRequestId = safeRequestId(req.body?.requestId);
      if (agentHarness.enabled || agentHarness.required) {
        const requestedTaskId = cleanText(req.body?.harnessTaskId, 80);
        requestedHarnessTaskId = requestedTaskId || null;
        if (requestedTaskId) {
          const existing = await agentHarness.getTask(requestedTaskId);
          exportHarnessTask = existing?.task || null;
          exportHarnessSequence = Math.max(
            0,
            ...(Array.isArray(existing?.events) ? existing.events.map((item) => Number(item.sequence) || 0) : [0]),
          );
          if (!exportHarnessTask
            || exportHarnessTask.actor_user_id !== req.workspace.user.id
            || exportHarnessTask.capability !== "courseware") {
            throw new AuthError("课件任务不存在或无权访问", "COURSEWARE_TASK_FORBIDDEN", 403);
          }
          if (["WAITING_REVIEW", "APPROVED"].includes(exportHarnessTask.status)) {
            const existingFiles = (await store.listFiles()).filter(
              (item) => item.ownerUserId === req.workspace.user.id
                && item.harnessTaskId === exportHarnessTask.id
                && item.status === "ready",
            );
            const existingPptx = existingFiles.find((item) => item.artifactType === "courseware_pptx");
            const existingNotes = existingFiles.find((item) => item.artifactType === "courseware_notes");
            const existingWeb = existingFiles.find((item) => item.artifactType === "courseware_guizang_html");
            if (existingPptx && existingNotes && existingWeb) {
              const publicTask = publicHarnessState(exportHarnessTask);
              res.json({
                ok: true,
                file: serializeFile(existingPptx),
                notesFile: serializeFile(existingNotes),
                webFile: serializeFile(existingWeb),
                slideCount: Number(existingPptx.slideCount || 0),
                task: { id: exportHarnessTask.id, version: exportHarnessTask.version, ...publicTask },
                replayed: true,
              });
              return;
            }
          }
          if (!["QUEUED", "RUNNING"].includes(exportHarnessTask.status)) {
            const parentTask = exportHarnessTask;
            const recoveryRequestKey = `courseware-export:${requestedTaskId}:${exportRequestId}`;
            const recovered = await agentHarness.createTask({
              requestKey: recoveryRequestKey,
              actorUserId: req.workspace.user.id,
              conversationId: parentTask.conversation_id || "courseware-export",
              capability: "courseware",
              title: spec.title,
              requestSha256: sha256Summary(`${requestedTaskId}\n${spec.title}\n${redactSensitive(req.body?.content)}`),
              riskLevel: "low",
              requiresReview: true,
              executorType: "worker",
              modelId: null,
              metadata: {
                contract: "workspace-courseware.v1",
                entrypoint: "courseware_export_recovery",
                parent_task_id: requestedTaskId,
              },
            });
            exportHarnessTask = recovered?.task || null;
            exportHarnessSequence = 0;
            exportHarnessEventBase = recoveryRequestKey;
            if (!exportHarnessTask) {
              throw Object.assign(new Error("课件恢复任务创建失败，请重试"), { statusCode: 503, code: "COURSEWARE_RECOVERY_TASK_MISSING" });
            }
            const recordRecoverySetup = async (phase, summary) => {
              exportHarnessSequence += 1;
              const result = await agentHarness.recordEvent(exportHarnessTask.id, {
                eventKey: `${recoveryRequestKey}:${phase.toLowerCase()}`,
                phase,
                sequence: exportHarnessSequence,
                summarySha256: sha256Summary(summary),
                executorType: "worker",
                metadata: { contract: "workspace-courseware.v1", parent_task_id: requestedTaskId },
              });
              exportHarnessTask = result?.task || exportHarnessTask;
            };
            await recordRecoverySetup("CLASSIFIED", "courseware:recover-terminal-export");
            await recordRecoverySetup("PLANNED", "courseware-plan:recover-render-verify-review");
            await recordRecoverySetup("EXECUTING", "courseware-recover-render:worker");
          }
        } else {
          const requestKey = `courseware-export:${exportRequestId}`;
          const created = await agentHarness.createTask({
            requestKey,
            actorUserId: req.workspace.user.id,
            conversationId: "courseware-export",
            capability: "courseware",
            title: spec.title,
            requestSha256: sha256Summary(`${spec.title}\n${redactSensitive(req.body?.content)}`),
            riskLevel: "low",
            requiresReview: true,
            executorType: "worker",
            modelId: null,
            metadata: { contract: "workspace-courseware.v1", entrypoint: "courseware_export" },
          });
          exportHarnessTask = created?.task || null;
          exportHarnessEventBase = requestKey;
          const recordSetup = async (phase, summary) => {
            exportHarnessSequence += 1;
            const result = await agentHarness.recordEvent(exportHarnessTask.id, {
              eventKey: `${requestKey}:${phase.toLowerCase()}`,
              phase,
              sequence: exportHarnessSequence,
              summarySha256: sha256Summary(summary),
              executorType: "worker",
              metadata: { contract: "workspace-courseware.v1" },
            });
            exportHarnessTask = result?.task || exportHarnessTask;
          };
          await recordSetup("CLASSIFIED", "courseware:standalone-export");
          await recordSetup("PLANNED", "courseware-plan:render-pptx-notes-verify-review");
          await recordSetup("EXECUTING", "courseware-render:worker");
        }
        exportHarnessEventBase ||= `${exportHarnessTask.request_key}:export:${exportRequestId}`;
      }
      const [pptx, notes, webDeck] = await Promise.all([
        buildCoursewarePptx(spec),
        Promise.resolve(buildSpeakerNotes(spec)),
        buildCoursewareGuizangHtml(spec),
      ]);
      const pptxPath = join(artifactDir, `${randomUUID()}.pptx`);
      const notesPath = join(artifactDir, `${randomUUID()}.md`);
      const webPath = join(artifactDir, `${randomUUID()}.html`);
      await writeFile(pptxPath, pptx, { mode: 0o600 });
      writtenPaths.push(pptxPath);
      await writeFile(notesPath, notes, { mode: 0o600 });
      writtenPaths.push(notesPath);
      await writeFile(webPath, webDeck, { mode: 0o600 });
      writtenPaths.push(webPath);

      const { file, notesFile, webFile } = await store.transaction((draft) => {
        const timestamp = new Date().toISOString();
        const common = {
          ownerUserId: req.workspace.user.id,
          departmentId: req.workspace.user.departmentId || null,
          scope: "personal",
          space: "课程课件",
          status: "ready",
          createdAt: timestamp,
          updatedAt: timestamp,
        };
        const createdFile = {
          id: randomUUID(),
          ...common,
          originalName: pptx.filename,
          storagePath: pptxPath,
          mimeType: pptx.contentType,
          size: pptx.length,
          sha256: createHash("sha256").update(pptx).digest("hex"),
          artifactType: "courseware_pptx",
          slideCount: spec.slides.length,
          ...(exportHarnessTask ? { harnessTaskId: exportHarnessTask.id } : {}),
        };
        const createdNotesFile = {
          id: randomUUID(),
          ...common,
          originalName: notes.filename,
          storagePath: notesPath,
          mimeType: notes.contentType,
          size: notes.length,
          sha256: createHash("sha256").update(notes).digest("hex"),
          artifactType: "courseware_notes",
          ...(exportHarnessTask ? { harnessTaskId: exportHarnessTask.id } : {}),
        };
        const createdWebFile = {
          id: randomUUID(),
          ...common,
          originalName: webDeck.filename,
          storagePath: webPath,
          mimeType: webDeck.contentType,
          size: webDeck.length,
          sha256: createHash("sha256").update(webDeck).digest("hex"),
          artifactType: "courseware_guizang_html",
          slideCount: spec.slides.length,
          ...(exportHarnessTask ? { harnessTaskId: exportHarnessTask.id } : {}),
        };
        draft.files.push(createdFile, createdNotesFile, createdWebFile);
        draft.auditEvents.push({
          id: randomUUID(),
          action: "courseware.exported",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: createdFile.id,
          metadata: { notesFileId: createdNotesFile.id, webFileId: createdWebFile.id, slideCount: spec.slides.length, skill: "guizang-ppt-skill" },
          createdAt: timestamp,
          updatedAt: timestamp,
        });
        return { file: createdFile, notesFile: createdNotesFile, webFile: createdWebFile };
      });
      storedFileIds.push(file.id, notesFile.id, webFile.id);
      if (exportHarnessTask) {
        recordExportPhase = async (phase, summary, extra = {}) => {
          exportHarnessSequence += 1;
          const result = await agentHarness.recordEvent(exportHarnessTask.id, {
            eventKey: `${exportHarnessEventBase}:${extra.eventKeySuffix || phase.toLowerCase()}`,
            eventType: extra.eventType || "phase",
            phase,
            sequence: exportHarnessSequence,
            summarySha256: sha256Summary(summary),
            executorType: "worker",
            artifactRefs: extra.artifactRefs || [],
            metadata: extra.metadata || {},
            errorCode: extra.errorCode || null,
            errorMessage: extra.errorMessage || null,
          });
          exportHarnessTask = result?.task || exportHarnessTask;
        };
        const artifactRefs = [file, notesFile, webFile].map((item) => ({
          artifact_id: item.id,
          artifact_type: item.artifactType,
          storage_ref: `/workspace-api/files/${item.id}/download`,
          sha256: item.sha256,
          size_bytes: item.size,
        }));
        artifactCheckpointMayExist = true;
        await recordExportPhase(
          "CHECKPOINTED",
          `courseware-artifacts:${artifactRefs.map((item) => item.sha256).join(":")}`,
          {
            eventType: "artifact",
            eventKeySuffix: "artifacts",
            artifactRefs,
            metadata: { checkpoint: "artifacts_ready", slide_count: spec.slides.length },
          },
        );
        await recordExportPhase(
          "VERIFYING",
          `courseware-verify:${spec.slides.length}:${pptx.length}:${notes.length}:${webDeck.length}`,
          { metadata: { checks: ["format", "file_size", "sha256", "slide_count"] } },
        );
        await recordExportPhase(
          "FINISHED",
          `courseware-finished:${artifactRefs.map((item) => item.sha256).join(":")}`,
          { metadata: { result: "waiting_human_review" } },
        );
      }
      if (requestedHarnessTaskId) {
        const ownerMessages = await store.listMessages({ ownerUserId: req.workspace.user.id });
        const sourceMessage = [...ownerMessages].reverse().find(
          (item) => item.role === "assistant" && item.harnessTaskId === requestedHarnessTaskId,
        );
        if (sourceMessage) {
          await store.updateMessage(sourceMessage.id, {
            coursewareAction: null,
            coursewareFiles: [serializeFile(file), serializeFile(notesFile), serializeFile(webFile)],
            coursewareArtifactTaskId: exportHarnessTask?.id || null,
            ...(exportHarnessTask ? {
              harnessTaskStatus: exportHarnessTask.status,
              harnessTaskVersion: exportHarnessTask.version,
              harnessPublicState: publicHarnessState(exportHarnessTask),
            } : {}),
          });
        }
      }
      const publicTask = exportHarnessTask ? publicHarnessState(exportHarnessTask) : null;
      res.status(201).json({
        ok: true,
        file: serializeFile(file),
        notesFile: serializeFile(notesFile),
        webFile: serializeFile(webFile),
        slideCount: spec.slides.length,
        ...(exportHarnessTask ? { task: { id: exportHarnessTask.id, version: exportHarnessTask.version, ...publicTask } } : {}),
      });
    } catch (error) {
      const errorCode = safeDiagnosticCode(error?.code, "COURSEWARE_EXPORT_FAILED");
      if (exportHarnessTask
        && recordExportPhase
        && !["APPROVED", "REJECTED", "FAILED", "CANCELLED", "WAITING_REVIEW"].includes(exportHarnessTask.status)) {
        await recordExportPhase(
          "RUN_FAILED",
          `courseware-export-failed:${errorCode}`,
          {
            eventType: "failure",
            eventKeySuffix: `failed:${errorCode.toLowerCase()}`,
            errorCode,
            errorMessage: cleanText(error?.message, 300) || "课件导出失败",
            metadata: { artifact_checkpoint_may_exist: artifactCheckpointMayExist },
          },
        ).catch((harnessError) => console.error("agent_harness_export_failure_event_failed", {
          code: safeDiagnosticCode(harnessError?.code, "AGENT_HARNESS_EXPORT_FAILURE_EVENT_FAILED"),
        }));
      }
      if (!artifactCheckpointMayExist) {
        for (const fileId of storedFileIds) await store.delete("files", fileId).catch(() => {});
        for (const path of writtenPaths) await rm(path, { force: true }).catch(() => {});
      }
      if (!error.statusCode && ["TypeError", "RangeError"].includes(error.name)) error.statusCode = 400;
      next(error);
    } finally {
      activeArtifactUsers.delete(req.workspace.user.id);
      await releaseStorageReservation(req.artifactReservation);
    }
  });

  app.post("/workspace-api/courseware/tasks/:taskId/review", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      if (!agentHarness.enabled) {
        throw Object.assign(new Error("任务中枢尚未连接，请联系管理员"), { statusCode: 503, code: "AGENT_HARNESS_NOT_CONFIGURED" });
      }
      const current = await agentHarness.getTask(req.params.taskId);
      const task = current?.task;
      const isPreExecutionReview = task?.approval_stage === "pre_execution";
      const canReviewPreExecution = isPreExecutionReview && req.workspace.user.role === "admin";
      const ownsTask = task?.actor_user_id === req.workspace.user.id;
      if (!task || task.capability !== "courseware" || (!ownsTask && !canReviewPreExecution)) {
        throw new AuthError("课件任务不存在或无权访问", "COURSEWARE_TASK_FORBIDDEN", 403);
      }
      if (isPreExecutionReview && req.workspace.user.role !== "admin") {
        throw new AuthError("高风险课件任务只能由管理员审批", "COURSEWARE_PRE_EXECUTION_REVIEW_FORBIDDEN", 403);
      }
      if (task.status !== "WAITING_REVIEW" || !["pre_execution", "result"].includes(task.approval_stage)) {
        throw Object.assign(new Error("当前课件不在待审批状态"), { statusCode: 409, code: "COURSEWARE_TASK_NOT_WAITING_REVIEW" });
      }
      const reviewStage = task.approval_stage;
      const decision = cleanText(req.body?.decision, 20);
      if (!["approve", "reject"].includes(decision)) {
        throw Object.assign(new Error("审批决定必须明确为 approve 或 reject"), {
          statusCode: 400,
          code: "COURSEWARE_REVIEW_DECISION_REQUIRED",
        });
      }
      const reviewed = await agentHarness.reviewTask(task.id, {
        decision,
        taskVersion: Number(task.version),
        reviewerUserId: req.workspace.user.id,
        reasonCodes: Array.isArray(req.body?.reasonCodes) ? req.body.reasonCodes.slice(0, 20) : [],
        note: cleanText(req.body?.note, 1_000) || null,
      });
      await store.addAudit({
        action: reviewStage === "pre_execution"
          ? decision === "approve" ? "courseware.execution_approved" : "courseware.execution_rejected"
          : decision === "approve" ? "courseware.approved" : "courseware.rejected",
        actorUserId: req.workspace.user.id,
        targetType: "workspace_task",
        targetId: task.id,
        metadata: { decision, reviewStage, taskVersion: task.version },
      });
      const publicTask = publicHarnessState(reviewed?.task || task);
      res.json({
        ok: true,
        task: {
          id: task.id,
          version: reviewed?.task?.version || task.version,
          approvalStage: reviewed?.task?.approval_stage || null,
          ...publicTask,
        },
      });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/files", requireUser, async (req, res) => {
    const files = (await store.listFiles()).filter((file) => !file.groupId && file.scope !== "group");
    const visible = req.workspace.user.role === "admin"
      ? files
      : req.workspace.user.role === "manager"
        ? files.filter((file) => file.ownerUserId === req.workspace.user.id || (file.scope === "department" && file.departmentId && file.departmentId === req.workspace.user.departmentId))
        : files.filter((file) => file.ownerUserId === req.workspace.user.id);
    visible.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
    res.json({ ok: true, files: visible.map(serializeFile) });
  });

  app.post("/workspace-api/files", requireUser, uploadLimiter, reserveUploadStorage, upload.single("file"), async (req, res, next) => {
    let storedFile;
    try {
      if (!req.file) throw Object.assign(new Error("请选择文件"), { statusCode: 400 });
      if (req.body?.confirmCompanyOwned !== "true") {
        throw Object.assign(new Error("请先确认资料属于公司内部办公范围且不含客户交付内容"), {
          statusCode: 400,
          code: "UPLOAD_SCOPE_CONFIRMATION_REQUIRED",
        });
      }
      const malwareScanStatus = requireMalwareScan ? await scanUpload(req.file.path) : "not_required";
      const submitAsCompanyKnowledge = req.body?.submitAsCompanyKnowledge === "true";
      const knowledgeType = cleanText(req.body?.knowledgeType, 80);
      if (submitAsCompanyKnowledge && !KNOWLEDGE_TYPES.has(knowledgeType)) {
        throw Object.assign(new Error("请选择公司知识类型"), { statusCode: 400, code: "KNOWLEDGE_TYPE_REQUIRED" });
      }
      const requestedScope = FILE_SCOPES.has(req.body?.knowledgeVisibility) ? req.body.knowledgeVisibility : "company";
      const scope = submitAsCompanyKnowledge ? requestedScope : "personal";
      if (scope === "department" && !req.workspace.user.departmentId) {
        throw Object.assign(new Error("当前账号尚未分配部门"), { statusCode: 400 });
      }
      const buffer = await readFile(req.file.path);
      const sha256 = createHash("sha256").update(buffer).digest("hex");
      const allFiles = await store.listFiles();
      const exactDuplicates = allFiles.filter((file) => file.sha256 === sha256 && file.status !== "deleted");
      if (!submitAsCompanyKnowledge && exactDuplicates.some((file) => file.ownerUserId === req.workspace.user.id)) {
        throw Object.assign(new Error("该文件已经上传"), { statusCode: 409 });
      }
      const fileId = randomUUID();
      const fileCreatedAt = new Date().toISOString();
      const persistentStorage = submitAsCompanyKnowledge
        ? { storageBackend: "server_disk", storagePath: req.file.path }
        : await persistWorkspaceFile({
            fileId,
            ownerUserId: req.workspace.user.id,
            originalName: req.file.originalname,
            sourcePath: req.file.path,
            mimeType: req.file.mimetype,
            sha256,
            createdAt: fileCreatedAt,
          });
      storedFile = await store.createFile({
        id: fileId,
        createdAt: fileCreatedAt,
        ownerUserId: req.workspace.user.id,
        departmentId: req.workspace.user.departmentId || null,
        originalName: req.file.originalname,
        ...persistentStorage,
        mimeType: req.file.mimetype,
        size: req.file.size,
        sha256,
        scope,
        space: cleanText(req.body?.space, 80) || "我的资料",
        status: "processing",
        malwareScanStatus,
        submitAsCompanyKnowledge,
        knowledgeType: submitAsCompanyKnowledge ? knowledgeType : null,
        workflowStatus: submitAsCompanyKnowledge ? "automatic_precheck" : "ordinary_attachment",
      });
      if (!submitAsCompanyKnowledge) {
        storedFile = await store.updateFile(storedFile.id, { status: "ready", workflowStatus: "ordinary_attachment" });
        if (persistentStorage.storageBackend === "oss") await rm(req.file.path, { force: true });
        await store.addAudit({
          action: "workspace.file_uploaded",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: storedFile.id,
          metadata: { scope: "personal", size: storedFile.size, sha256, submitAsCompanyKnowledge: false },
        });
        return res.status(201).json({ ok: true, file: serializeFile(storedFile) });
      }
      const extracted = await extractAndChunkDocument({ name: req.file.originalname, mimeType: req.file.mimetype, buffer }, { idPrefix: storedFile.id });
      if (!extracted.text) throw Object.assign(new Error("文件没有可识别的文字内容"), { statusCode: 422 });
      if (containsRestrictedBusinessContent(extracted.text)) {
        throw Object.assign(new Error("该资料不在员工办公知识范围内"), { statusCode: 403, code: "KNOWLEDGE_SCOPE_DENIED" });
      }
      const visibility = scope;
      const submittedSensitiveCategories = parseStringList(req.body?.sensitiveCategories, SENSITIVE_CATEGORIES);
      const detectedSensitiveCategories = detectSensitiveCategories(extracted.text);
      const sensitiveCategories = [...new Set([...submittedSensitiveCategories, ...detectedSensitiveCategories])];
      const sensitive = req.body?.sensitive === "true" || sensitiveCategories.length > 0;
      const normalizedBaseName = req.file.originalname.replace(/(?:[-_ ]?v?\d+(?:\.\d+)*)?(\.[^.]+)$/iu, "$1").toLowerCase();
      const versionConflicts = allFiles.filter((file) => file.status !== "deleted"
        && String(file.originalName || "").replace(/(?:[-_ ]?v?\d+(?:\.\d+)*)?(\.[^.]+)$/iu, "$1").toLowerCase() === normalizedBaseName);
      const timestamp = new Date().toISOString();
      const document = await store.createKnowledgeDocument({
        fileId: storedFile.id,
        title: req.file.originalname,
        ownerUserId: req.workspace.user.id,
        ownerId: req.workspace.user.id,
        departmentId: req.workspace.user.departmentId || null,
        visibility,
        scope: visibility,
        space: cleanText(req.body?.space, 80) || "我的资料",
        status: "pending_review",
        workflowStatus: "pending_human_review",
        knowledgeType,
        aiSummary: redactSensitive(extracted.text).replace(/\s+/gu, " ").trim().slice(0, 360),
        duplicateCheck: {
          exactDuplicate: exactDuplicates.length > 0,
          exactDuplicateCount: exactDuplicates.length,
          versionConflict: versionConflicts.length > 0,
          versionConflictCount: versionConflicts.length,
        },
        sensitive,
        sensitiveCategories,
        requiresAdminReview: sensitive,
        suggestedPermission: visibility,
        source: "员工上传",
        sourcePath: `受限缓冲区/${storedFile.id}`,
        targetDirectory: `公司知识/${knowledgeType}`,
        automaticPrecheckAt: timestamp,
        workflowHistory: [
          { status: "uploaded", at: storedFile.createdAt, actorUserId: req.workspace.user.id },
          { status: "automatic_precheck", at: timestamp, actorUserId: null },
          { status: "pending_human_review", at: timestamp, actorUserId: null },
        ],
      });
      await store.addKnowledgeChunks(document.id, extracted.chunks.map((chunk) => ({
        content: redactSensitive(chunk.content),
        position: chunk.index,
      })));
      storedFile = await store.updateFile(storedFile.id, {
        status: "pending_review",
        workflowStatus: "pending_human_review",
        knowledgeDocumentId: document.id,
        sensitive,
        sensitiveCategories,
      });
      await store.addAudit({
        action: "workspace.file_uploaded",
        actorUserId: req.workspace.user.id,
        targetType: "file",
        targetId: storedFile.id,
        metadata: {
          scope,
          size: storedFile.size,
          sha256,
          submitAsCompanyKnowledge: true,
          knowledgeType,
          sensitive,
          source: "员工上传",
          targetDirectory: "受限缓冲区",
          permissionScope: visibility,
        },
      });
      res.status(201).json({ ok: true, file: serializeFile(storedFile) });
    } catch (error) {
      if (error.code === "MALWARE_DETECTED") {
        await store.addAudit({
          action: "workspace.file_malware_blocked",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: null,
          metadata: { blocked: true },
        }).catch(() => {});
      }
      if (storedFile) await store.updateFile(storedFile.id, error.code === "KNOWLEDGE_SCOPE_DENIED"
        ? { status: "quarantined", dataClassification: "restricted_customer_data", customerDataDetected: true, quarantineReason: "customer_scope" }
        : { status: "failed", errorCode: error.code || "EXTRACTION_FAILED" }).catch(() => {});
      if (req.file?.path) await rm(req.file.path, { force: true }).catch(() => {});
      next(error);
    }
  });

  app.get("/workspace-api/files/:id/download", requireUser, async (req, res, next) => {
    try {
      const file = await store.getFile(req.params.id);
      let allowed = Boolean(file && canAccessFile(file, req.workspace.user));
      if (file?.groupId) {
        try {
          await getVisibleGroup(file.groupId, req.workspace.user);
          allowed = true;
        } catch {
          allowed = false;
        }
      }
      if (!file || !["ready", "pending_review"].includes(file.status) || !allowed) {
        throw new AuthError("文件不存在或无权下载", "FILE_FORBIDDEN", 404);
      }
      if (file.status === "pending_review" && file.malwareScanStatus !== "clean") {
        throw new AuthError("文件尚未通过安全检查", "FILE_SCAN_REQUIRED", 423);
      }
      if (file.storageBackend === "oss") {
        const signedUrl = await workspaceObjectStore.signedGetUrl({
          key: file.storageKey,
          filename: file.originalName,
          disposition: "attachment",
          contentType: file.mimeType,
        });
        await store.addAudit({
          action: "workspace.file_downloaded",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: file.id,
          metadata: { artifactType: file.artifactType || "uploaded_document", storageBackend: "oss" },
        }).catch(() => {});
        return res.redirect(302, signedUrl);
      }
      const canonicalPath = await resolveStoredUpload(file);
      res.download(canonicalPath, file.originalName, async (error) => {
        if (error) {
          if (!res.headersSent) next(error);
          return;
        }
        await store.addAudit({
          action: "workspace.file_downloaded",
          actorUserId: req.workspace.user.id,
          targetType: "file",
          targetId: file.id,
          metadata: { artifactType: file.artifactType || "uploaded_document" },
        }).catch(() => {});
      });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/files/:id/preview", requireUser, async (req, res, next) => {
    try {
      const file = await store.getFile(req.params.id);
      const extension = extname(file?.originalName || "").toLowerCase();
      const contentType = GENERATED_IMAGE_MIME_BY_EXTENSION.get(extension);
      if (!file || file.status !== "ready" || !contentType) {
        throw new AuthError("图片不存在或无权预览", "FILE_PREVIEW_FORBIDDEN", 404);
      }
      let allowed = file.ownerUserId === req.workspace.user.id
        && ["ordinary_chat_attachment", "lovart_generated_image", "libimage_generated_image", "knowledge_card_generated"].includes(file.workflowStatus);
      if (req.workspace.user.role === "admin" && ["lovart_generated_image", "libimage_generated_image", "knowledge_card_generated"].includes(file.workflowStatus)) allowed = true;
      if (file.groupId) {
        try {
          await getVisibleGroup(file.groupId, req.workspace.user);
          allowed = true;
        } catch {
          allowed = false;
        }
      }
      if (!allowed) {
        throw new AuthError("图片不存在或无权预览", "FILE_PREVIEW_FORBIDDEN", 404);
      }
      if (file.storageBackend === "oss") {
        const signedUrl = await workspaceObjectStore.signedGetUrl({
          key: file.storageKey,
          filename: file.originalName,
          disposition: "inline",
          contentType,
        });
        return res.redirect(302, signedUrl);
      }
      const canonicalPath = await resolveStoredUpload(file);
      res.sendFile(canonicalPath, {
        headers: {
          "content-type": contentType,
          "content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(file.originalName || "group-image")}`,
          "cache-control": "private, max-age=60",
        },
      });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/knowledge", requireUser, async (req, res) => {
    const [documents, chunks, externalDocuments] = await Promise.all([
      store.listKnowledgeDocuments({ status: "published" }),
      store.listKnowledgeChunks(),
      externalKnowledge.listDocuments().catch(() => []),
    ]);
    const full = [...buildKnowledgeDocuments(documents.filter((document) => !document.externalized), chunks), ...externalDocuments];
    const visible = full.filter((document) => searchKnowledge(document.title || "资料", [document], req.workspace.user, { limit: 1, minimumScore: 0 }).length > 0);
    res.json({ ok: true, documents: visible.map(({ chunks: _chunks, ...document }) => document) });
  });

  app.get("/workspace-api/knowhow/board", requireUser, async (req, res) => {
    const [items, runs] = await Promise.all([
      store.listKnowhowItems(),
      store.listKnowhowRuns(),
    ]);
    const visibleItems = items
      .filter((item) => item.visibility !== "boss" || req.workspace.user.role === "admin")
      .filter((item) => !item.sensitive)
      .sort((left, right) => String(right.createdAt).localeCompare(String(left.createdAt)));
    const today = new Date().toISOString().slice(0, 10);
    const stats = {
      today: visibleItems.filter((item) => String(item.createdAt || "").startsWith(today)).length,
      pending: visibleItems.filter((item) => item.status === "pending_review").length,
      published: visibleItems.filter((item) => item.status === "published").length,
      streams: KNOWHOW_STREAMS.length,
    };
    const visibleRuns = runs.sort((left, right) => String(right.createdAt).localeCompare(String(left.createdAt))).slice(0, 20);
    const streamSummaries = KNOWHOW_STREAMS.map((stream) => {
      const outcome = visibleRuns.flatMap((run) => Array.isArray(run.outcomes) ? run.outcomes : [])
        .find((item) => item.streamId === stream.id);
      return {
        id: stream.id,
        label: stream.label,
        lastRunAt: outcome ? visibleRuns.find((run) => run.outcomes?.includes(outcome))?.finishedAt || null : null,
        lastStatus: outcome?.status || "idle",
        createdCount: outcome?.createdCount || 0,
        failedCount: outcome?.failedCount || 0,
      };
    });
    res.json({
      ok: true,
      stats,
      items: visibleItems.slice(0, 200),
      collector: { ...knowhowCollectorState, intervalMinutes: Math.round(knowhowCollectionIntervalMs / 60_000) },
      streams: streamSummaries,
      runs: visibleRuns,
      canReview: ["admin", "manager"].includes(req.workspace.user.role),
    });
  });

  app.post("/workspace-api/knowhow/sources", requireUser, requireKnowledgeReviewer, async (req, res, next) => {
    try {
      throw Object.assign(new Error("采集范围由系统自动管理"), { statusCode: 410, code: "KNOWHOW_SOURCE_CONFIGURATION_REMOVED" });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/knowhow/collect", requireUser, requireKnowledgeReviewer, async (req, res, next) => {
    try {
      const run = await runKnowhowCollection({ actorUserId: req.workspace.user.id, trigger: "manual" });
      res.status(201).json({ ok: true, run });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/knowhow/items", requireUser, async (req, res, next) => {
    try {
      const title = redactSensitive(cleanText(req.body?.title, 160));
      const summary = redactSensitive(cleanText(req.body?.summary, 2_000));
      if (!title) throw Object.assign(new Error("请填写知识标题"), { statusCode: 400, code: "KNOWHOW_TITLE_REQUIRED" });
      if (!summary) throw Object.assign(new Error("请填写知识摘要"), { statusCode: 400, code: "KNOWHOW_SUMMARY_REQUIRED" });
      const sourceUrl = cleanText(req.body?.sourceUrl, 600);
      if (sourceUrl) validateKnowhowFetchUrl(sourceUrl);
      const item = await store.createKnowhowItem({
        title,
        summary,
        sourceUrl: sourceUrl || null,
        category: redactSensitive(cleanText(req.body?.category || "其他", 80)),
        vertical: redactSensitive(cleanText(req.body?.vertical || "服装", 80)),
        tags: parseStringList(req.body?.tags, new Set(String(req.body?.tags || "").split(/[,，]/u).map((value) => cleanText(value, 40)).filter(Boolean))),
        ownerUserId: req.workspace.user.id,
        ownerName: req.workspace.user.displayName || req.workspace.user.email,
        submittedBy: "workspace",
        reviewedBy: null,
        reviewedAt: null,
      });
      await store.addAudit({
        action: "workspace.knowhow_item_created",
        actorUserId: req.workspace.user.id,
        targetType: "knowhow_item",
        targetId: item.id,
        metadata: { title: item.title, category: item.category },
      });
      res.status(201).json({ ok: true, item });
    } catch (error) { next(error); }
  });

  app.patch("/workspace-api/knowhow/items/:id", requireUser, requireKnowledgeReviewer, async (req, res, next) => {
    try {
      const item = await store.getKnowhowItem(req.params.id);
      if (!item) throw Object.assign(new Error("Know-how内容不存在"), { statusCode: 404, code: "KNOWHOW_ITEM_NOT_FOUND" });
      if (req.body?.confirm !== true) throw Object.assign(new Error("请二次确认本次操作"), { statusCode: 400, code: "KNOWHOW_REVIEW_CONFIRMATION_REQUIRED" });
      const action = ["publish", "reject", "archive"].includes(req.body?.action) ? req.body.action : null;
      if (!action) throw Object.assign(new Error("请选择发布、退回或存档"), { statusCode: 400, code: "KNOWHOW_ACTION_REQUIRED" });
      const reason = redactSensitive(cleanText(req.body?.reason, 500));
      if (action === "reject" && !reason) throw Object.assign(new Error("退回时请填写原因"), { statusCode: 400, code: "KNOWHOW_REJECT_REASON_REQUIRED" });
      const status = action === "publish" ? "published" : action === "reject" ? "rejected" : "archived";
      const updated = await store.updateKnowhowItem(item.id, {
        status,
        reviewedBy: req.workspace.user.id,
        reviewedAt: new Date().toISOString(),
        reviewReason: reason || null,
        publishedAt: action === "publish" ? new Date().toISOString() : item.publishedAt || null,
      });
      const knowledgeDocument = await syncKnowhowKnowledgeDocument(updated, req.workspace.user.id);
      await store.addAudit({
        action: "workspace.knowhow_item_reviewed",
        actorUserId: req.workspace.user.id,
        targetType: "knowhow_item",
        targetId: item.id,
        metadata: { action, reason: reason || null },
      });
      res.json({ ok: true, item: updated, knowledgeDocument });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/system/knowledge-sync/pending", requireKnowledgeSyncAgent, async (req, res) => {
    const limit = Math.max(1, Math.min(50, Number(req.query?.limit) || 20));
    const documents = await store.listKnowledgeDocuments();
    const candidates = documents
      .filter((document) => document.status === "approved"
        && ["nas_sync_pending", "nas_sync_failed"].includes(document.workflowStatus)
        && document.syncRetryable !== false
        && !document.customerDataDetected
        && document.dataClassification !== "restricted_customer_data")
      .sort((left, right) => String(left.reviewedAt || left.updatedAt).localeCompare(String(right.reviewedAt || right.updatedAt)))
      .slice(0, limit);
    const items = [];
    for (const document of candidates) {
      try {
        items.push((await syncBundle(document.id)).item);
      } catch (error) {
        if (error?.code !== "SYNC_FILE_NOT_READY") throw error;
      }
    }
    res.json({ ok: true, items });
  });

  app.get("/workspace-api/system/knowledge-sync/items/:id", requireKnowledgeSyncAgent, async (req, res, next) => {
    try {
      res.json({ ok: true, item: (await syncBundle(req.params.id, { includeChunks: true })).item });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/system/knowledge-sync/items/:id/file", requireKnowledgeSyncAgent, async (req, res, next) => {
    try {
      const { file } = await syncBundle(req.params.id);
      const canonicalPath = await resolveStoredUpload(file);
      const buffer = await readFile(canonicalPath);
      const actualSha256 = createHash("sha256").update(buffer).digest("hex");
      if (!secureEqualText(actualSha256, file.sha256)) {
        throw Object.assign(new Error("待同步文件校验失败"), { statusCode: 409, code: "SYNC_FILE_HASH_MISMATCH" });
      }
      res.set({
        "content-type": file.mimeType || "application/octet-stream",
        "content-length": String(buffer.length),
        "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(file.originalName || "knowledge-file")}`,
        "x-content-sha256": actualSha256,
        "cache-control": "no-store",
      });
      res.send(buffer);
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/system/knowledge-sync/items/:id/complete", requireKnowledgeSyncAgent, async (req, res, next) => {
    try {
      const { document, file } = await syncBundle(req.params.id);
      const sourceSha256 = cleanText(req.body?.sourceSha256, 80).toLowerCase();
      const cardSha256 = cleanText(req.body?.cardSha256, 80).toLowerCase();
      const indexContentSha256 = cleanText(req.body?.indexContentSha256, 80).toLowerCase();
      if (!/^[a-f0-9]{64}$/u.test(sourceSha256) || !secureEqualText(sourceSha256, file.sha256)) {
        throw Object.assign(new Error("NAS 文件校验值不一致"), { statusCode: 409, code: "SYNC_SOURCE_HASH_MISMATCH" });
      }
      if (!/^[a-f0-9]{64}$/u.test(cardSha256) || !/^[a-f0-9]{64}$/u.test(indexContentSha256)) {
        throw Object.assign(new Error("知识卡或索引校验值无效"), { statusCode: 400, code: "SYNC_CHECKSUM_REQUIRED" });
      }
      const targetPath = cleanRelativeKnowledgePath(req.body?.targetPath);
      const cardPath = cleanRelativeKnowledgePath(req.body?.cardPath);
      const indexGeneratedAt = new Date(req.body?.indexGeneratedAt || "");
      if (!Number.isFinite(indexGeneratedAt.getTime())) {
        throw Object.assign(new Error("索引生成时间无效"), { statusCode: 400, code: "SYNC_INDEX_TIME_REQUIRED" });
      }
      const timestamp = new Date().toISOString();
      const workflowHistory = [
        ...(Array.isArray(document.workflowHistory) ? document.workflowHistory : []),
        { status: "nas_synced", at: timestamp, actorUserId: null, targetPath, cardPath },
        { status: "index_completed", at: timestamp, actorUserId: null, contentSha256: indexContentSha256 },
      ];
      const updated = await store.updateKnowledgeDocument(document.id, {
        status: "published",
        workflowStatus: "index_completed",
        source: "NAS企业知识库",
        sourcePath: cardPath,
        targetDirectory: targetPath,
        externalized: true,
        nasSyncedAt: timestamp,
        nasFilePath: targetPath,
        nasCardPath: cardPath,
        nasCardSha256: cardSha256,
        indexGeneratedAt: indexGeneratedAt.toISOString(),
        indexContentSha256,
        indexDocuments: Math.max(0, Number(req.body?.indexDocuments) || 0),
        indexChunks: Math.max(0, Number(req.body?.indexChunks) || 0),
        syncRetryable: false,
        syncError: null,
        workflowHistory,
      });
      await store.updateFile(file.id, {
        status: "ready",
        workflowStatus: "index_completed",
        nasSyncedAt: timestamp,
        nasStoragePath: targetPath,
      });
      await store.addAudit({
        action: "workspace.knowledge_nas_synced",
        actorUserId: null,
        targetType: "knowledge",
        targetId: document.id,
        metadata: {
          sourceSha256,
          cardSha256,
          targetPath,
          cardPath,
          indexGeneratedAt: indexGeneratedAt.toISOString(),
          indexContentSha256,
          indexDocuments: updated.indexDocuments,
          indexChunks: updated.indexChunks,
        },
      });
      externalKnowledge.invalidate();
      res.json({ ok: true, document: updated });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/system/knowledge-sync/items/:id/fail", requireKnowledgeSyncAgent, async (req, res, next) => {
    try {
      const { document } = await syncBundle(req.params.id);
      const reason = cleanText(req.body?.reason, 600);
      if (!reason) throw Object.assign(new Error("请提供同步失败原因"), { statusCode: 400, code: "SYNC_FAILURE_REASON_REQUIRED" });
      const retryable = req.body?.retryable !== false;
      const timestamp = new Date().toISOString();
      const workflowStatus = retryable ? "nas_sync_failed" : "nas_sync_blocked";
      const updated = await store.updateKnowledgeDocument(document.id, {
        workflowStatus,
        syncRetryable: retryable,
        syncError: reason,
        workflowHistory: [
          ...(Array.isArray(document.workflowHistory) ? document.workflowHistory : []),
          { status: workflowStatus, at: timestamp, actorUserId: null, reason },
        ],
      });
      await store.updateFile(document.fileId, { workflowStatus });
      await store.addAudit({
        action: "workspace.knowledge_nas_sync_failed",
        actorUserId: null,
        targetType: "knowledge",
        targetId: document.id,
        metadata: { reason, retryable },
      });
      res.json({ ok: true, document: updated });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/admin/users", requireUser, requireAdmin, async (_req, res) => {
    const users = await store.listUsers();
    const initializedUsers = [];
    for (const user of users) {
      initializedUsers.push((await store.ensureCreditAccount(user.id, imageInitialCredits)).user);
    }
    res.json({ ok: true, users: initializedUsers.map(serializeUser) });
  });

  app.post("/workspace-api/admin/users", requireUser, requireAdmin, async (req, res, next) => {
    try {
      const user = await auth.createUser({
        email: cleanText(req.body?.email, 240),
        displayName: cleanText(req.body?.name || req.body?.displayName, 80),
        password: String(req.body?.password || ""),
        departmentId: cleanText(req.body?.departmentId, 80) || null,
        role: ["admin", "manager", "employee"].includes(req.body?.role) ? req.body.role : "employee",
      }, { actorUserId: req.workspace.user.id });
      const initialized = await store.ensureCreditAccount(user.id, imageInitialCredits);
      res.status(201).json({ ok: true, user: serializeUser(initialized.user) });
    } catch (error) { next(error); }
  });

  app.patch("/workspace-api/admin/users/:id", requireUser, requireAdmin, async (req, res, next) => {
    try {
      const patch = {};
      if (req.body?.enabled === true) patch.status = "active";
      if (req.body?.enabled === false) {
        if (req.body?.confirm !== true) {
          throw Object.assign(new Error("请二次确认停用员工账号"), { statusCode: 400, code: "ACCOUNT_DISABLE_CONFIRMATION_REQUIRED" });
        }
        patch.status = "disabled";
      }
      if (typeof req.body?.name === "string") patch.displayName = cleanText(req.body.name, 80);
      if (typeof req.body?.departmentId === "string" || req.body?.departmentId === null) patch.departmentId = req.body.departmentId || null;
      if (["admin", "manager", "employee"].includes(req.body?.role)) patch.role = req.body.role;
      if (req.body?.role !== undefined && !["admin", "manager", "employee"].includes(req.body.role)) {
        throw Object.assign(new Error("员工角色无效"), { statusCode: 400 });
      }
      if (req.params.id === req.workspace.user.id && (patch.status === "disabled" || (patch.role && patch.role !== "admin"))) {
        throw Object.assign(new Error("不能停用或降级当前管理员账号"), { statusCode: 400 });
      }
      const user = await store.transaction((draft) => {
        const target = draft.users.find((candidate) => candidate.id === req.params.id);
        if (!target) throw Object.assign(new Error("员工账号不存在"), { statusCode: 404 });
        const nextStatus = patch.status || target.status;
        const nextRole = patch.role || target.role;
        if (target.role === "admin" && target.status === "active" && (nextRole !== "admin" || nextStatus !== "active")) {
          const activeAdmins = draft.users.filter((candidate) => candidate.role === "admin" && candidate.status === "active").length;
          if (activeAdmins <= 1) throw Object.assign(new Error("必须至少保留一名已启用管理员"), { statusCode: 409, code: "LAST_ADMIN_REQUIRED" });
        }
        const timestamp = new Date().toISOString();
        Object.assign(target, patch, { updatedAt: timestamp });
        if (nextStatus !== "active") {
          for (const session of draft.sessions) {
            if (session.userId === target.id && !session.revokedAt) {
              session.revokedAt = timestamp;
              session.updatedAt = timestamp;
            }
          }
        }
        draft.auditEvents.push({
          id: randomUUID(),
          action: "workspace.user_updated",
          actorUserId: req.workspace.user.id,
          targetType: "user",
          targetId: target.id,
          metadata: { fields: Object.keys(patch) },
          createdAt: timestamp,
          updatedAt: timestamp,
        });
        return target;
      });
      res.json({ ok: true, user: serializeUser(user) });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/admin/users/:id/reset-password", requireUser, requireAdmin, async (req, res, next) => {
    try {
      const user = await auth.setPassword(req.params.id, String(req.body?.password || ""), { actorUserId: req.workspace.user.id });
      res.json({ ok: true, user: serializeUser(user) });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/admin/credits/:id/adjust", requireUser, requireAdmin, mutationLimiter, async (req, res, next) => {
    try {
      const amount = Math.trunc(Number(req.body?.amount));
      const note = cleanText(req.body?.note, 160);
      if (!Number.isInteger(amount) || amount === 0 || Math.abs(amount) > 10_000) {
        throw Object.assign(new Error("积分调整必须是 -10000 到 10000 的非零整数"), { statusCode: 400, code: "INVALID_CREDIT_ADJUSTMENT" });
      }
      if (!note) throw Object.assign(new Error("请填写积分调整原因"), { statusCode: 400, code: "CREDIT_NOTE_REQUIRED" });
      await store.ensureCreditAccount(req.params.id, imageInitialCredits);
      const adjusted = await store.adjustCredits({
        userId: req.params.id,
        amount,
        note,
        actorUserId: req.workspace.user.id,
      });
      await store.addAudit({
        action: "workspace.credits_adjusted",
        actorUserId: req.workspace.user.id,
        targetType: "user",
        targetId: req.params.id,
        metadata: { amount, balanceAfter: adjusted.balance, note },
      });
      res.json({ ok: true, user: serializeUser(adjusted.user), transaction: adjusted.transaction });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/admin/credits/transactions", requireUser, requireAdmin, async (_req, res) => {
    const [transactions, users] = await Promise.all([
      store.listCreditTransactions(),
      store.listUsers(),
    ]);
    const userMap = new Map(users.map((user) => [user.id, serializeUser(user)]));
    res.json({
      ok: true,
      transactions: transactions.slice(0, 500).map((transaction) => ({
        ...transaction,
        user: userMap.get(transaction.userId) || null,
        actor: userMap.get(transaction.actorUserId) || null,
      })),
    });
  });

  app.get("/workspace-api/admin/audit", requireUser, requireAdmin, async (_req, res) => {
    const events = await store.listAudit();
    res.json({ ok: true, events: events.slice(0, 500) });
  });

  app.get("/workspace-api/admin/knowledge", requireUser, requireKnowledgeReviewer, async (req, res) => {
    const [documents, files, users, chunks] = await Promise.all([
      store.listKnowledgeDocuments(),
      store.listFiles(),
      store.listUsers(),
      store.listKnowledgeChunks(),
    ]);
    const fileMap = new Map(files.map((file) => [file.id, file]));
    const userMap = new Map(users.map((user) => [user.id, serializeUser(user)]));
    const previewMap = new Map();
    for (const chunk of chunks) {
      if (!previewMap.has(chunk.documentId)) previewMap.set(chunk.documentId, []);
      const pieces = previewMap.get(chunk.documentId);
      if (pieces.join(" ").length < 800) pieces.push(cleanText(chunk.content, 800));
    }
    const visibleDocuments = req.workspace.user.role === "admin"
      ? documents
      : documents.filter((document) => document.visibility === "department"
        && document.departmentId
        && document.departmentId === req.workspace.user.departmentId
        && !document.requiresAdminReview
        && !document.sensitive);
    const result = visibleDocuments
      .map((document) => ({
        ...document,
        file: document.fileId ? serializeFile(fileMap.get(document.fileId)) : null,
        fileHash: document.fileId ? fileMap.get(document.fileId)?.sha256 || null : null,
        owner: userMap.get(document.ownerUserId) || null,
        previewText: redactSensitive((previewMap.get(document.id) || []).join(" ")).slice(0, 800),
      }))
      .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
    res.json({ ok: true, documents: result });
  });

  app.patch("/workspace-api/admin/knowledge/:id", requireUser, requireKnowledgeReviewer, async (req, res, next) => {
    try {
      const document = await store.getKnowledgeDocument(req.params.id);
      if (!document) throw Object.assign(new Error("资料不存在"), { statusCode: 404 });
      if (req.workspace.user.role === "manager" && (!document.departmentId || document.departmentId !== req.workspace.user.departmentId || document.visibility !== "department")) {
        throw new AuthError("无权审核该资料", "FORBIDDEN", 403);
      }
      if (document.customerDataDetected || document.dataClassification === "restricted_customer_data") {
        throw Object.assign(new Error("受限资料不能发布到员工知识库"), { statusCode: 403, code: "KNOWLEDGE_SCOPE_DENIED" });
      }
      if ((document.requiresAdminReview || document.sensitive) && req.workspace.user.role !== "admin") {
        throw new AuthError("该资料需由指定管理员复核", "ADMIN_REVIEW_REQUIRED", 403);
      }
      if (document.status !== "pending_review") {
        throw Object.assign(new Error("该资料已处理，请刷新列表"), { statusCode: 409, code: "KNOWLEDGE_ALREADY_REVIEWED" });
      }
      const action = ["approve", "archive", "reject"].includes(req.body?.action)
        ? req.body.action
        : ({ published: "approve", archived: "archive", rejected: "reject" }[req.body?.status] || null);
      if (!action) throw Object.assign(new Error("请选择审核处理方式"), { statusCode: 400 });
      if (req.body?.confirm !== true) throw Object.assign(new Error("请二次确认本次操作"), { statusCode: 400, code: "REVIEW_CONFIRMATION_REQUIRED" });
      const reason = cleanText(req.body?.reason, 600);
      if (action === "reject" && !reason) throw Object.assign(new Error("请填写退回原因"), { statusCode: 400, code: "REJECT_REASON_REQUIRED" });
      const retentionDays = Math.max(7, Math.min(30, Number(req.body?.retentionDays) || 14));
      const timestamp = new Date().toISOString();
      const status = action === "approve" ? "approved" : action === "archive" ? "archived" : "rejected";
      const workflowStatus = action === "approve" ? "nas_sync_pending" : status;
      const workflowHistory = [
        ...(Array.isArray(document.workflowHistory) ? document.workflowHistory : []),
        { status, at: timestamp, actorUserId: req.workspace.user.id, reason: reason || null },
        ...(action === "approve" ? [{ status: "nas_sync_pending", at: timestamp, actorUserId: null }] : []),
      ];
      const quarantineUntil = action === "reject" ? new Date(Date.now() + retentionDays * 24 * 60 * 60 * 1000).toISOString() : null;
      const updated = await store.updateKnowledgeDocument(document.id, {
        status,
        workflowStatus,
        reviewedBy: req.workspace.user.id,
        reviewedAt: timestamp,
        reviewReason: reason || null,
        retentionDays: action === "reject" ? retentionDays : null,
        quarantineUntil,
        workflowHistory,
      });
      const file = document.fileId ? await store.getFile(document.fileId) : null;
      if (document.fileId) await store.updateFile(document.fileId, {
        status: action === "reject" ? "quarantined" : status,
        workflowStatus,
        quarantineUntil,
      });
      await store.addAudit({
        action: "workspace.knowledge_reviewed",
        actorUserId: req.workspace.user.id,
        targetType: "knowledge",
        targetId: document.id,
        metadata: {
          action,
          status,
          reason: reason || null,
          sha256: file?.sha256 || null,
          targetDirectory: action === "approve" ? document.targetDirectory : action === "archive" ? "项目与档案目录" : "隔离与回收区",
          permissionScope: document.suggestedPermission || document.visibility,
          retentionDays: action === "reject" ? retentionDays : null,
        },
      });
      res.json({ ok: true, document: updated });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/admin/model-policy", requireUser, requireAdmin, async (_req, res) => {
    const policies = await store.listModelPolicies({ key: "default" });
    const policy = policies[0] || null;
    const models = await getTextModelStatus();
    res.json({
      ok: true,
      policy: policy ? {
        ...policy,
        defaultModel: normalizeTextModelId(policy.defaultModel || policy.defaultMode),
        models,
      } : null,
    });
  });

  app.patch("/workspace-api/admin/model-policy", requireUser, requireAdmin, async (req, res, next) => {
    try {
      const policies = await store.listModelPolicies({ key: "default" });
      const policy = policies[0];
      if (!policy) throw Object.assign(new Error("策略不存在"), { statusCode: 404 });
      const patch = {};
      if (TEXT_MODEL_IDS.has(req.body?.defaultModel)) patch.defaultModel = req.body.defaultModel;
      if (typeof req.body?.allowEmployeeSwitch === "boolean") patch.allowEmployeeSwitch = req.body.allowEmployeeSwitch;
      if (Number.isFinite(Number(req.body?.dailyUserLimit))) patch.dailyUserLimit = Math.max(1, Math.min(1000, Number(req.body.dailyUserLimit)));
      const updated = await store.updateModelPolicy(policy.id, patch);
      await store.addAudit({ action: "workspace.model_policy_updated", actorUserId: req.workspace.user.id, targetType: "model_policy", targetId: policy.id, metadata: { fields: Object.keys(patch) } });
      res.json({ ok: true, policy: { ...updated, models: await getTextModelStatus() } });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/admin/wecom/directory/sync", requireUser, requireAdmin, mutationLimiter, async (_req, res) => {
    const sync = await syncWeComDirectory({ force: true });
    if (sync.status === "error") {
      return res.status(503).json({
        ok: false,
        error: "企业微信通讯录同步失败，请检查应用可见范围和通讯录权限",
        code: sync.errorCode || "WECOM_DIRECTORY_SYNC_FAILED",
      });
    }
    res.json({ ok: true, sync });
  });

  app.get("/workspace-api/admin/wecom/content/setup", requireUser, requireAdmin, (_req, res, next) => {
    if (!wecomContentEnabled) {
      return next(Object.assign(new Error("企业微信内容调度尚未启用"), { statusCode: 503, code: "WECOM_CONTENT_NOT_CONFIGURED" }));
    }
    res.setHeader("cache-control", "private, no-store, max-age=0");
    res.json({
      ok: true,
      callbackUrl: "https://sail-crm.chuangchuang.cn/workspace-api/wecom/content/callback",
      token: wecomCallbackConfig.token,
      encodingAesKey: wecomCallbackConfig.encodingAesKey,
      instruction: "将这三项粘贴到企业微信应用的接收消息配置，并选择已启用加密模式。",
    });
  });

  if (wecomDirectoryState.enabled) {
    queueMicrotask(() => { void syncWeComDirectory({ force: true }); });
    wecomDirectorySyncTimer = setInterval(() => {
      void syncWeComDirectory({ force: true });
    }, wecomDirectorySyncIntervalMs);
    wecomDirectorySyncTimer.unref();
  }

  if (crmOutreach.enabled) {
    queueMicrotask(() => { void maintainOutreachReviewQueue(); });
    outreachMaintenanceTimer = setInterval(() => {
      void maintainOutreachReviewQueue();
    }, 60_000);
    outreachMaintenanceTimer.unref();
  }

  if (knowhowAutoCollect) {
    queueMicrotask(() => { void runKnowhowCollection({ trigger: "startup" }).catch(() => {}); });
    knowhowCollectionTimer = setInterval(() => {
      void runKnowhowCollection({ trigger: "scheduler" }).catch(() => {});
    }, knowhowCollectionIntervalMs);
    knowhowCollectionTimer.unref();
  }

  if (lovartRemoteEnabled) {
    queueMicrotask(() => { void maintainRemoteImageJobs(); });
    imageRemoteJobWatchdogTimer = setInterval(() => {
      void maintainRemoteImageJobs().catch((error) => {
        logDiagnostic("warn", "image_executor_watchdog_failed", {
          code: safeDiagnosticCode(error?.code || "IMAGE_WATCHDOG_FAILED"),
        });
      });
    }, imageRemoteJobWatchdogIntervalMs);
    imageRemoteJobWatchdogTimer.unref();
  }

  queueMicrotask(() => { void purgeExpiredImageReferenceCache().catch(() => {}); });
  imageReferenceCacheTimer = setInterval(() => {
    void purgeExpiredImageReferenceCache().catch((error) => {
      logDiagnostic("warn", "image_reference_cache_cleanup_failed", {
        code: safeDiagnosticCode(error?.code || "IMAGE_REFERENCE_CACHE_CLEANUP_FAILED"),
      });
    });
  }, Math.min(imageReferenceCacheTtlMs, 5 * 60 * 1000));
  imageReferenceCacheTimer.unref();

  app.get("/workspace-api/wechat-editor/workflow", requireUser, async (req, res, next) => {
    try {
      const workflow = await wechatRuns.getDefaultWorkflow(req.workspace.user.id);
      res.json({ ok: true, workflow: publicWechatWorkflow(workflow) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/wechat-editor/runs", requireUser, async (req, res, next) => {
    try {
      const runs = await wechatRuns.list({ ownerUserId: req.workspace.user.id });
      res.json({ ok: true, runs: runs.slice(0, 50).map(publicWechatRun) });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/wechat-editor/runs", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const topic = cleanText(req.body?.topic, 500);
      const sourceText = cleanText(req.body?.sourceText, 20_000);
      const idempotencyKey = cleanText(req.get("idempotency-key") || req.body?.idempotencyKey, 240);
      if (topic.length < 4) throw Object.assign(new Error("选题至少需要 4 个字"), { statusCode: 400, code: "WECHAT_TOPIC_REQUIRED" });
      if (!idempotencyKey || !/^[A-Za-z0-9._:-]{8,240}$/u.test(idempotencyKey)) {
        throw Object.assign(new Error("公众号任务幂等键无效"), { statusCode: 400, code: "WECHAT_IDEMPOTENCY_KEY_INVALID" });
      }
      const workflow = await wechatRuns.getDefaultWorkflow(req.workspace.user.id);
      const run = await wechatRuns.create({
        ownerUserId: req.workspace.user.id,
        workflowId: workflow.id,
        workflowVersion: workflow.version,
        topic,
        prompt: topic,
        sourceText,
        idempotencyKey,
      });
      setImmediate(() => { void runWechatInBackground(run); });
      res.status(202).json({ ok: true, run: publicWechatRun(run) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/wechat-editor/runs/:id", requireUser, async (req, res, next) => {
    try {
      const run = await wechatRuns.get(cleanText(req.params.id, 160), req.workspace.user.id);
      if (!run) throw Object.assign(new Error("公众号任务不存在"), { statusCode: 404, code: "WECHAT_RUN_NOT_FOUND" });
      res.json({ ok: true, run: publicWechatRun(run) });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/wechat-editor/runs/:id/events", requireUser, async (req, res, next) => {
    try {
      const id = cleanText(req.params.id, 160);
      const run = await wechatRuns.get(id, req.workspace.user.id);
      if (!run) throw Object.assign(new Error("公众号任务不存在"), { statusCode: 404, code: "WECHAT_RUN_NOT_FOUND" });
      const events = await wechatRuns.events(id, req.workspace.user.id);
      res.json({ ok: true, events: publicWechatRun({ ...run, events }).events });
    } catch (error) { next(error); }
  });

  app.post("/workspace-api/wechat-editor/runs/:id/review", requireUser, mutationLimiter, async (req, res, next) => {
    try {
      const id = cleanText(req.params.id, 160);
      const decision = cleanText(req.body?.decision, 20).toLowerCase();
      const note = cleanText(req.body?.note, 1_000);
      const run = await wechatRuns.get(id, req.workspace.user.id);
      if (!run) throw Object.assign(new Error("公众号任务不存在"), { statusCode: 404, code: "WECHAT_RUN_NOT_FOUND" });
      if (!["approve", "reject"].includes(decision)) throw Object.assign(new Error("审核决定无效"), { statusCode: 400, code: "WECHAT_REVIEW_INVALID" });
      const already = decision === "approve" ? run.approval?.status === "approved" : run.approval?.status === "rejected";
      if (already) return res.json({ ok: true, run: publicWechatRun(run), replayed: true });
      if (run.status !== "WAITING_APPROVAL") throw Object.assign(new Error("当前任务不在待审核状态"), { statusCode: 409, code: "WECHAT_REVIEW_NOT_READY" });
      if (decision === "reject" && note.length < 2) throw Object.assign(new Error("退回时请填写修改意见"), { statusCode: 400, code: "WECHAT_REVIEW_NOTE_REQUIRED" });
      const result = await wechatRuns.appendEvent(id, {
        stageId: "approval",
        status: decision === "approve" ? "approved" : "rejected",
        progress: 100,
        executor: "Human Gate",
        tool: "owner-review",
        note,
        error: decision === "reject" ? note : undefined,
        errorCode: decision === "reject" ? "WECHAT_REVIEW_REJECTED" : undefined,
        decidedByUserId: req.workspace.user.id,
        decidedAt: new Date().toISOString(),
      }, req.workspace.user.id);
      res.json({ ok: true, run: publicWechatRun(result.run), replayed: result.replayed });
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/wechat-editor/runs/:id/preview", requireUser, async (req, res, next) => {
    try {
      const run = await wechatRuns.get(cleanText(req.params.id, 160), req.workspace.user.id);
      if (!run) throw Object.assign(new Error("公众号任务不存在"), { statusCode: 404, code: "WECHAT_RUN_NOT_FOUND" });
      const artifact = run.artifacts?.find((item) => item.kind === "preview");
      if (!artifact) throw Object.assign(new Error("HTML 预览尚未生成"), { statusCode: 409, code: "WECHAT_PREVIEW_NOT_READY" });
      const path = safeWechatArtifactPath(artifact.relativePath);
      const info = await stat(path);
      if (!info.isFile()) throw Object.assign(new Error("HTML 预览无效"), { statusCode: 409, code: "WECHAT_PREVIEW_INVALID" });
      res.type("html").set("Cache-Control", "no-store");
      createReadStream(path).pipe(res);
    } catch (error) { next(error); }
  });

  app.get("/workspace-api/wechat-editor/runs/:id/package", requireUser, async (req, res, next) => {
    try {
      const id = cleanText(req.params.id, 160);
      let run = await wechatRuns.get(id, req.workspace.user.id);
      if (!run) throw Object.assign(new Error("公众号任务不存在"), { statusCode: 404, code: "WECHAT_RUN_NOT_FOUND" });
      if (run.approval?.status !== "approved") throw Object.assign(new Error("老板审核通过后才能导出发布包"), { statusCode: 409, code: "WECHAT_APPROVAL_REQUIRED" });
      let zipArtifact = run.artifacts?.find((item) => item.kind === "package");
      if (!zipArtifact) {
        const preview = run.artifacts?.find((item) => item.kind === "preview");
        const body = run.artifacts?.find((item) => item.kind === "body");
        const cover = run.artifacts?.find((item) => item.kind === "cover");
        if (!run.article || !preview || !body || !cover) throw Object.assign(new Error("发布包所需产物不完整"), { statusCode: 409, code: "WECHAT_PACKAGE_INPUTS_MISSING" });
        await wechatRuns.appendEvent(id, { stageId: "package", status: "running", progress: 10, executor: "Packager", tool: "adm-zip", inputSummary: "正在校验并生成发布包" }, req.workspace.user.id);
        try {
          const result = await buildWechatPackage({
            article: run.article,
            previewHtml: await readFile(safeWechatArtifactPath(preview.relativePath), "utf8"),
            bodyHtml: await readFile(safeWechatArtifactPath(body.relativePath), "utf8"),
            coverPath: safeWechatArtifactPath(cover.relativePath),
            imagePaths: run.artifacts.filter((item) => item.kind === "image").map((item) => safeWechatArtifactPath(item.relativePath)),
            events: await wechatRuns.events(id, req.workspace.user.id),
            outputRoot: join(wechatArtifactDir, "packages", id),
          });
          zipArtifact = { kind: "package", fileName: result.fileName, relativePath: relative(resolve(wechatArtifactDir), resolve(result.zipPath)), mimeType: "application/zip", sizeBytes: result.sizeBytes, sha256: result.sha256 };
          const completed = await wechatRuns.appendEvent(id, { stageId: "package", status: "succeeded", progress: 100, executor: "Packager", tool: "adm-zip", outputSummary: "发布包已生成并完成 SHA-256 校验", artifact: zipArtifact, artifacts: [...run.artifacts, zipArtifact] }, req.workspace.user.id);
          run = completed.run;
        } catch (error) {
          await wechatRuns.appendEvent(id, { stageId: "package", status: "failed", executor: "Packager", tool: "adm-zip", error: cleanText(error?.message || "发布包生成失败", 500), errorCode: cleanText(error?.code || "WECHAT_PACKAGE_FAILED", 160) }, req.workspace.user.id).catch(() => {});
          throw error;
        }
      }
      const zipPath = safeWechatArtifactPath(zipArtifact.relativePath);
      const info = await stat(zipPath);
      if (!info.isFile()) throw Object.assign(new Error("发布包文件无效"), { statusCode: 409, code: "WECHAT_PACKAGE_INVALID" });
      res.download(zipPath, basename(zipArtifact.fileName || zipPath));
    } catch (error) { next(error); }
  });

  queueMicrotask(async () => {
    const recoverable = await store.list("wechatRuns").catch(() => []);
    for (const run of recoverable.filter((item) => ["QUEUED", "RUNNING"].includes(item.status))) {
      void runWechatInBackground(run);
    }
  });

    const staticDir = resolve(options.staticDir || join(projectRoot, "dist"));
    app.use("/workspace", express.static(staticDir, {
      index: "index.html",
      maxAge: "1h",
      setHeaders(res, filePath) {
        if (filePath.endsWith("/index.html")) res.setHeader("Cache-Control", "no-cache");
      },
    }));
    app.use("/workspace", (req, res, next) => {
      if (req.method !== "GET" || extname(req.path)) return next();
      res.setHeader("Cache-Control", "no-cache");
      res.sendFile(join(staticDir, "index.html"));
    });

  app.use((req, res) => res.status(404).json({ ok: false, error: "页面或接口不存在", code: "NOT_FOUND" }));
  app.use((error, req, res, _next) => {
    const safe = safeError(error);
    logDiagnostic(safe.status >= 500 ? "error" : "warn", "request_failed", {
      requestId: req.workspaceRequestId || randomUUID(),
      method: req.method,
      route: diagnosticRoute(req),
      statusCode: safe.status,
      code: safeDiagnosticCode(error?.code || error?.name || "ERROR"),
    });
    res.status(safe.status).json({ ok: false, error: safe.message, code: safe.code });
  });

  return {
    app,
    store,
    geoStore,
    geoService,
    auth,
    dataDir,
    syncWeComDirectory,
    beginShutdown() {
      lifecycleState.shuttingDown = true;
    },
    logDiagnostic,
    dispose() {
      if (wecomDirectorySyncTimer) clearInterval(wecomDirectorySyncTimer);
      wecomDirectorySyncTimer = null;
      if (outreachMaintenanceTimer) clearInterval(outreachMaintenanceTimer);
      outreachMaintenanceTimer = null;
      if (knowhowCollectionTimer) clearInterval(knowhowCollectionTimer);
      knowhowCollectionTimer = null;
      if (imageRemoteJobWatchdogTimer) clearInterval(imageRemoteJobWatchdogTimer);
      imageRemoteJobWatchdogTimer = null;
      if (imageReferenceCacheTimer) clearInterval(imageReferenceCacheTimer);
      imageReferenceCacheTimer = null;
      chatProgressHub.close();
      geoService.dispose?.();
    },
  };
}

export async function startServer(options = {}) {
  const { app, ...runtime } = await createApp(options);
  const host = options.host || process.env.HOST || "127.0.0.1";
  const port = Number(options.port ?? process.env.PORT ?? 4311);
  const server = createServer(app);
  // 原片上传允许较大文件，避免 Node 默认 5 分钟请求超时中断慢速上传。
  server.requestTimeout = Math.max(server.requestTimeout || 0, numberFromEnv("AUTO_CUT_REQUEST_TIMEOUT_MS", 15 * 60 * 1000));
  const shutdownTimeoutMs = Math.max(1_000, Number(
    options.shutdownTimeoutMs ?? process.env.GRACEFUL_SHUTDOWN_TIMEOUT_MS ?? 15_000,
  ) || 15_000);
  let shutdownPromise = null;
  let drainingConnections = false;
  const signalHandlers = new Map();
  const removeSignalHandlers = () => {
    for (const [signal, handler] of signalHandlers) process.off(signal, handler);
    signalHandlers.clear();
  };
  server.once("close", () => runtime.dispose?.());
  server.on("request", (req, res) => {
    res.once("finish", () => {
      if (drainingConnections && !req.socket.destroyed) req.socket.end();
    });
  });
  await new Promise((resolveListen, reject) => {
    server.once("error", reject);
    server.listen(port, host, () => { server.off("error", reject); resolveListen(); });
  });
  const shutdown = (signal = "manual") => {
    if (shutdownPromise) return shutdownPromise;
    runtime.beginShutdown?.();
    drainingConnections = true;
    runtime.logDiagnostic?.("info", "shutdown_started", { signal: safeDiagnosticCode(signal, "MANUAL") });
    shutdownPromise = new Promise((resolveShutdown, rejectShutdown) => {
      const forceTimer = setTimeout(() => {
        runtime.logDiagnostic?.("warn", "shutdown_force_closing_connections", { timeoutMs: shutdownTimeoutMs });
        server.closeAllConnections?.();
      }, shutdownTimeoutMs);
      forceTimer.unref();
      server.close((error) => {
        clearTimeout(forceTimer);
        removeSignalHandlers();
        if (error) {
          runtime.logDiagnostic?.("error", "shutdown_failed", { code: safeDiagnosticCode(error.code || error.name || "ERROR") });
          rejectShutdown(error);
          return;
        }
        runtime.logDiagnostic?.("info", "shutdown_completed");
        resolveShutdown();
      });
      server.closeIdleConnections?.();
    });
    return shutdownPromise;
  };
  if (options.installSignalHandlers === true) {
    for (const signal of ["SIGTERM", "SIGINT"]) {
      const handler = () => {
        void shutdown(signal).catch(() => { process.exitCode = 1; });
      };
      signalHandlers.set(signal, handler);
      process.once(signal, handler);
    }
  }
  return { server, app, ...runtime, shutdown, host, port: server.address().port };
}

if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
  startServer({ installSignalHandlers: true }).then(({ host, port }) => {
    console.log(`创创员工 AI 工作台已启动：http://${host}:${port}/workspace/`);
  }).catch((error) => {
    console.error("工作台启动失败", error?.message || error);
    process.exitCode = 1;
  });
}
