{
  "id": "Next30Office2026",
  "name": "Office proof - LOCAL model and record review",
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "id": "start",
      "name": "手动开始",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "prepare",
      "name": "预检记录",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "parameters": {
        "jsCode": "const bundles = [{\"source\": {\"record_id\": \"DEMO-RECORD-A\", \"fields\": {\"原文\": [{\"type\": \"text\", \"text\": \"青禾台灯底座松动，\"}, {\"type\": \"text\", \"text\": \"请售后联系确认。\"}], \"输入版本\": 1}}, \"current\": {\"record_id\": \"DEMO-RECORD-A\", \"fields\": {\"原文\": \"青禾台灯底座松动，请售后联系确认。\", \"输入版本\": 1, \"处理状态\": \"待处理\"}}, \"model_output\": {\"summary\": \"青禾台灯底座松动，需要售后联系确认。\"}}, {\"source\": {\"record_id\": \"DEMO-RECORD-B\", \"fields\": {\"原文\": \"退款到账时间需要核对。\", \"输入版本\": 1}}, \"current\": {\"record_id\": \"DEMO-RECORD-B\", \"fields\": {\"原文\": \"退款已经到账。\", \"输入版本\": 2, \"处理状态\": \"待处理\"}}, \"model_output\": {\"summary\": \"需要核对退款到账时间。\"}}, {\"source\": {\"record_id\": \"DEMO-RECORD-C\", \"fields\": {\"原文\": \"申请修改收货地址。\", \"输入版本\": 1}}, \"current\": {\"record_id\": \"DEMO-RECORD-C\", \"fields\": {\"原文\": \"申请修改收货地址。\", \"输入版本\": 1, \"处理状态\": \"完成\", \"处理版本\": 1}}, \"model_output\": {\"summary\": \"申请修改收货地址。\"}}];\nfunction plainText(value) {\n  if (typeof value === \"string\") return value;\n  if (!Array.isArray(value) || value.some((part) => !part || part.type !== \"text\" || typeof part.text !== \"string\")) throw new Error(\"Expected a text field; attachment, person and link fields need their own mapping\");\n  return value.map((part) => part.text).join(\"\");\n}\n\nfunction prepareFeishuReview(bundles) {\n  if (!Array.isArray(bundles) || bundles.length > 100) throw new Error(\"Expected at most 100 record bundles\");\n  const seen = new Set();\n  return bundles.map(({ source, current, model_output }) => {\n    if (!source || typeof source.record_id !== \"string\" || !source.record_id.trim() || seen.has(source.record_id)) throw new Error(\"Missing or duplicate record_id\");\n    seen.add(source.record_id);\n    const text = plainText(source.fields?.[\"原文\"]);\n    const revision = source.fields?.[\"输入版本\"];\n    if (!Number.isSafeInteger(revision) || revision < 1 || !text.trim() || text.length > 4000) throw new Error(\"Invalid text or revision\");\n    const identity = { record_id: source.record_id, input_revision: revision };\n    if (!current || current.record_id !== source.record_id || current.fields?.[\"输入版本\"] !== revision || plainText(current.fields?.[\"原文\"]) !== text) return { ...identity, decision: \"stale\", write_allowed: false };\n    if (current.fields?.[\"处理状态\"] === \"完成\" && current.fields?.[\"处理版本\"] === revision) return { ...identity, decision: \"already_done\", write_allowed: false };\n    const result = typeof model_output === \"string\" ? JSON.parse(model_output) : model_output;\n    if (!result || Object.keys(result).sort().join(\",\") !== \"summary\" || typeof result.summary !== \"string\" || !result.summary.trim() || Array.from(result.summary).length > 100 || /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f]/.test(result.summary)) throw new Error(\"Expected exactly one bounded summary\");\n    return { ...identity, fields: { \"AI摘要\": result.summary, \"处理状态\": \"待审核\", \"处理版本\": revision }, decision: \"awaiting_human\", write_allowed: false };\n  });\n}\n\nconst planning=prepareFeishuReview(bundles);\nconst candidate=bundles[0];\nif(planning[0].decision!=='awaiting_human')throw Error('No eligible record');\nreturn [{json:{source:candidate.source,current:candidate.current,bundles,planning,backend:'LOCAL DEMO records, NOT Feishu data'}}];\n"
      }
    },
    {
      "id": "http",
      "name": "请求本地摘要模型",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        480,
        0
      ],
      "retryOnFail": false,
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:11435/v1/chat/completions",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ ({ model: 'tutorial-qwen3-proof:latest', messages: [{ role:'system', content:'仅返回JSON对象，唯一字段summary，不超过45个字。只摘要原文，不添加判断、赔偿、承诺或新事实。 /no_think' }, { role:'user', content:'原文：'+$json.source.fields['原文'].map(p=>p.text).join('') }], temperature:0, max_tokens:180, think:false, response_format: { type:'json_object' } }) }}",
        "options": {
          "timeout": 120000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "sendCredentialsOnCrossOriginRedirect": false
        }
      }
    },
    {
      "id": "review",
      "name": "核对版本并等待人工审核",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        0
      ],
      "parameters": {
        "jsCode": "function plainText(value) {\n  if (typeof value === \"string\") return value;\n  if (!Array.isArray(value) || value.some((part) => !part || part.type !== \"text\" || typeof part.text !== \"string\")) throw new Error(\"Expected a text field; attachment, person and link fields need their own mapping\");\n  return value.map((part) => part.text).join(\"\");\n}\n\nfunction prepareFeishuReview(bundles) {\n  if (!Array.isArray(bundles) || bundles.length > 100) throw new Error(\"Expected at most 100 record bundles\");\n  const seen = new Set();\n  return bundles.map(({ source, current, model_output }) => {\n    if (!source || typeof source.record_id !== \"string\" || !source.record_id.trim() || seen.has(source.record_id)) throw new Error(\"Missing or duplicate record_id\");\n    seen.add(source.record_id);\n    const text = plainText(source.fields?.[\"原文\"]);\n    const revision = source.fields?.[\"输入版本\"];\n    if (!Number.isSafeInteger(revision) || revision < 1 || !text.trim() || text.length > 4000) throw new Error(\"Invalid text or revision\");\n    const identity = { record_id: source.record_id, input_revision: revision };\n    if (!current || current.record_id !== source.record_id || current.fields?.[\"输入版本\"] !== revision || plainText(current.fields?.[\"原文\"]) !== text) return { ...identity, decision: \"stale\", write_allowed: false };\n    if (current.fields?.[\"处理状态\"] === \"完成\" && current.fields?.[\"处理版本\"] === revision) return { ...identity, decision: \"already_done\", write_allowed: false };\n    const result = typeof model_output === \"string\" ? JSON.parse(model_output) : model_output;\n    if (!result || Object.keys(result).sort().join(\",\") !== \"summary\" || typeof result.summary !== \"string\" || !result.summary.trim() || Array.from(result.summary).length > 100 || /[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f]/.test(result.summary)) throw new Error(\"Expected exactly one bounded summary\");\n    return { ...identity, fields: { \"AI摘要\": result.summary, \"处理状态\": \"待审核\", \"处理版本\": revision }, decision: \"awaiting_human\", write_allowed: false };\n  });\n}\n\nconst previous=$('预检记录').first().json;\nconst response=$input.first().json;\nif(response.choices?.[0]?.finish_reason!=='stop')throw Error('Incomplete local generation');\nconst actual=JSON.parse(response.choices[0].message.content);\nconst bundles=previous.bundles;\nbundles[0].model_output=actual;\nconst decisions=prepareFeishuReview(bundles);\nif(decisions.map(r=>r.decision).join(',')!=='awaiting_human,stale,already_done')throw Error('Unexpected revision branches');\nif(decisions.some(r=>r.write_allowed!==false))throw Error('Human approval bypass');\nreturn [{json:{actual_model_output:actual,decisions,finish_reason:response.choices[0].finish_reason,usage:response.usage,model:response.model,model_requests:1,feishu_requests:0,commercial_requests:0,writeback_executed:false,verified_backend:'Native n8n HTTP Request to isolated localhost Ollama; not Feishu or apizt'}}];\n"
      }
    }
  ],
  "connections": {
    "手动开始": {
      "main": [
        [
          {
            "node": "预检记录",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "预检记录": {
      "main": [
        [
          {
            "node": "请求本地摘要模型",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "请求本地摘要模型": {
      "main": [
        [
          {
            "node": "核对版本并等待人工审核",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}