--- name: skill-case-get description: "案例采集 / 拆解 / 归档。把各种素材(连续多图 / 九宫格、Word .docx、PPT .pptx、图文混合、裸图、视频)拆解为「案例信息」与「素材数组 materialAssets[]」,做合规判断(能否入库 / 客诉负面 / 隐私发现)与打标(学校别名归一 + 跟进状态七段 + 亮点/场景/异议),最后经云函数 caseSubmit 落到待审清单。适用场景:(1) 同事连发 6/9 张图其实是同一个素材,需要按九宫格阅读顺序编号归档;(2) 一份 Word/PPT 案例稿要抽出文字说明并拆出内嵌配图;(3) 判断这堆素材能不能进案例库、是不是客诉负面事件;(4) 按案例库标签体系打标并归一学校别名(IC → 帝国理工)。本 Skill 只处理已获授权素材。" description_en: "Case intake, decomposition and archiving. Turns raw material (image grids, .docx, .pptx, mixed text+image, bare images, video) into case fields plus an ordered materialAssets[] array, runs compliance triage and tagging, then submits to the review queue via the caseSubmit cloud function. Use for: (1) a run of 6/9 images that is ONE material needing 3x3 reading-order numbering, (2) extracting text and embedded media from Word/PPT, (3) deciding whether material may enter the case library or is a complaint/negative event, (4) tagging against the case-library tag system and normalising school aliases (IC -> Imperial College London). This skill only processes authorised material." version: 1.0.1 author: Yuyang001 (FmodeAgent) license: MPL-2.0 copyright: "Copyright (c) 2026 未来飞马 Fmode" tags: [未来飞马, 智能体技能, 超级技能, 服务级, 案例采集, 案例拆解, 素材归档, FmodeAgent, Hermes Agent, FmodeCode, Claude Code, case-library, case-intake, ocr, material-assets, 九宫格, 打标] --- 本 Skill 只处理已获授权素材。 # Fmode Case Get — 案例采集 / 拆解 / 归档 ## Overview 把素材变成**可入库的案例**: ``` 各种素材 → 拆解分析 → 能否进案例库判断 → 打标 → 上传归档(待审清单) ``` | 来源类型 | 处理方式 | | --- | --- | | ① 连续多张图片(朋友圈九宫格) | skill-vision OCR + 语义理解;按尺寸/时间/内容相似度**判为同一组**,按九宫格阅读顺序给显式 `order` | | ② Word `.docx` | python3 标准库 `zipfile` + `ElementTree` 解包:文字段落、表格、内嵌媒体、超链接 | | ③ PPT `.pptx` | 同上:按放映顺序抽每页标题/正文/备注/图片/表格 | | ④ 图文混合 | 文档文字走「案例说明」,内嵌图并进素材组一起编号 | | ⑤ 裸图片无文字说明 | 靠视觉理解补 `label` / `description` / `usageSuggestion`;分组回落到尺寸 + 时间信号 | | ⑥ 视频(附加) | ffmpeg 抽帧 → 交给图片链路;音轨走 skill-listen 转写 | **关键区分(必须分开,不能混)** - 「案例说明」——什么时候用、大概情况、背景 → 案例字段 `title / summary / targetCustomer / usageSuggestion / resultEvidence / outcome` - 「图片 / 视频」→ `materialAssets[]`(`order` 1 起,同组内按左上 → 右下) ## 红线(不可协商) 1. **授权前置(已废弃 — 2026-10-12 需求变更)**:授权字段已从案例库删除,**默认全部授权**,不再要求 `authorizationStatus`、不再校验。采集/审核/检索全链路都不看授权。若系统里仍出现授权闸门,属于回退,按「红线 6」处理。 2. **不进公共素材库**:采集只写 `reviewStatus='pending'` + `readyForUse=false`,由负责人用 `caseApprove` 放行后才进检索结果。 3. **不落 PII**:真实 PII 只在本次请求内存中处理。`privacyFindings[].snippet` **本身就是掩码后的**(`138****5678`),不打码不落库。 4. **不伪造结果**:缺 ffmpeg / skill-vision / skill-listen 时,在 `warnings` 里明说,不编造 OCR 或转写内容。 5. **不写凭据**:token 只从环境变量 / 本地配置读,仅内存持有,不落盘、不进日志、不进报告。 6. **打码仍是红线**:授权放宽 **不等于** 可以放未打码内容。检索侧强制 `maskingStatus ∈ {reviewed, REVIEWED}`,未打码的绝不进结果。 7. **图片 URL 必须同源**:落库的 `materialAssets[].url` 必须指向**看板同源**路径(`https://app.fmode.cn/dev//…`),不要用 `s3.fmode.cn` —— 后者**无 CORS 头**,前端 `fetch()` 取图必失败(看板的「复制图片」功能依赖同源取图)。采集时用项目内 OBS 路径上传素材,而不是个人空间的 s3 分享链接。 ## 鉴权与依赖 - **云函数**:`POST https://server.lumistedu.com/api/functions`,请求体 `{"path":"/caseSubmit","params":{...},"token":"..."}`。 session token 来源:`CASE_PARSE_SESSION_TOKEN` → `PARSE_SESSION_TOKEN`。 - **图片理解**:复用兄弟技能 `skill-vision` 的 `analyze()`(`~/.claude/skills/skill-vision/scripts/vision-client.mjs`)。 该函数在**宿主自带多模态**时(FmodeCode / Claude Code)会返回读图指令而非调 API,此时由宿主 Agent 用自己的 Read 工具读图后补全。 可用 `--vision-model ` 覆盖,或 `--skip-vision` 跳过。 - **音视频**:复用兄弟技能 `skill-listen` 的 `listen-runner.mjs`(走 Fmode 网关,凭据仅服务端)。 - **素材上传**:复用兄弟技能 `fmode-storage` 的 `uploader.mjs`(第 0 级云函数免密钥通道)。 找不到时 `uploadMaterial` 抛 `StorageUnavailableError`,ingest 会降级(`url` 留空 + 告警)而**不崩**。 可用 `CASE_STORAGE_SCRIPT` 指定其路径;本部署该技能在平台技能目录(`/opt/data/skills/fmode-storage`), `resolveSiblingScript` 已覆盖 `CASE_SKILLS_DIR` / `$HERMES_HOME/skills` / `/opt/data/skills`。 - **python3**:解包 `.docx` / `.pptx`,**只用标准库**(本机没有 python-docx / python-pptx)。 - **ffmpeg / ffprobe**:视频抽帧、探测时长(可选)。 > 缺 token 是「缺配置」,不是「用不了」——请勿点任何付费/充值弹窗。 ## 用法 ### 命令行 ```bash # 只有图片(九宫格):先看分组与顺序,不调模型 npx skill-case-get@latest plan -- --image g1.jpg --image g2.jpg ... --image g9.jpg # 完整采集:拆解 + 图片理解 + 打标,产出案例包(不提交) npx skill-case-get@latest ingest -- \ --image g1.jpg --image g9.jpg \ --doc 案例稿.docx --deck 复盘.pptx \ --authorization authorized \ --source-ref wx-group-2026-10 \ --asset-base-url https://cdn.example.com/cases # 只给一个目录,自动按扩展名分流 npx skill-case-get@latest plan -- --input ./素材目录 # 调顺序(把第 3 张挪到第 1 位) npx skill-case-get@latest reorder -- --material-assets /material-assets.json --move "3:1" # 提交到待审清单 npx skill-case-get@latest submit -- --package /case-package.json --submitted-by user123 # 自检(含 office 解包、九宫格分组、打标、合规、顺序调整) npx skill-case-get@latest selftest ``` `ingest` / `plan` 之后必须加 `--`,其后参数透传给运行器。 ### 在 Node 脚本中调用 技能目录被复制进 `.claude/skills/` 时**不含 node_modules**,运行器是零依赖 ESM: ```js import { runPlan, runIngest, runSubmit } from './scripts/case-intake.mjs'; const report = await runIngest({ inputs: ['g1.jpg', 'g2.jpg', '案例稿.docx'], outDir: './case-get-outputs/run-001', authorizationStatus: 'authorized', assetBaseUrl: 'https://cdn.example.com/cases', }); console.log(report.casePackage.materialAssets); // order 已按九宫格排好 console.log(report.casePackage.riskFlags); // 合规判断 if (report.canSubmit) await runSubmit({ package: report.artifacts.casePackage }); ``` ## 产物 | 文件 | 内容 | | --- | --- | | `extracted.json` | 文档文字段落 / 表格 / 超链接 / 内嵌媒体 / 视频转写 | | `group-plan.json` | 分组(`groupId` / `layout` / `count` / `orderingRule`)与组内顺序 | | `vision.json` | 每张图的 `ocrText` / `description` / `label` / `usageSuggestion` / `piiHints` | | `material-assets.json` | `materialAssets[]`(可手工调顺序后回灌) | | `case-package.json` | 云函数 `caseSubmit` 入参,**逐字对齐共享契约** | | `report.md` | 人类可读报告(顺序表 / 标签 / 合规 / 下一步) | ## 分组与顺序规则(能力 3) 同事连发 6 张、9 张图**不是多个素材,而是同一个素材**(朋友圈素材按九宫格排)。 三种信号判同组,**至少 2 种成立**才并组: | 信号 | 判据 | 阈值 | | --- | --- | --- | | 尺寸 | 宽高比相似度 | ≥ 0.88 | | 时间 | 文件时间差 | ≤ 15 分钟(窗口内线性衰减) | | 内容 | OCR/描述 字符二元组 Jaccard | ≥ 0.25 | - 只有 1 种信号成立 → **不并组**(避免把不同素材误并); - 完全拿不到尺寸/时间(纯转发图)时,会按现有信号降级判定,理由写进 `reasons`; - 顺序:`orderingRule = left-to-right,top-to-bottom`,`order` **1 起**连续编号; 原始排版位置丢失时用「收图时间升序 + 文件名自然序」近似发送顺序; - 要人工干预:`reorder --move "3:1"` 或 `--order "3,1,2"`,也可在输入 manifest 里写 `groups[].items[].order`。 九宫格 = `layout: "3x3"`;6 张 = `3x2`;4 张 = `2x2`。 ## 打标(能力 6) - **硬性静态属性**:`schoolCanonical` / `country` / `major` / `stage` / `subject` - **学校别名归一**:`SchoolAlias` 表(`IC → 帝国理工`、`UCL → 伦敦大学学院`)。 别名表存在 `references/tag-dictionary.json` 的 `schoolAlias.entries`;命中的写法写入 `schoolAliases[]`。 英文别名按词边界匹配(`IC` 不会命中 `MAGIC`)。 - **销售阶段七段**:`新进线 / 挖需中 / 方案推荐中 / 异议处理中 / 待决策 / 沉默待跟进 / 已成交` 按 `docs/prd/dashboard/10-案例推荐策略.md` 的策略表反推,写进 `fitStatus[]`。 - **亮点 / 场景 / 异议**:`highlightTypes` / `scenarioTags` / `objectionTags` - **字典自进化**:采集到字典里没有的新取值时追加进 `references/tag-dictionary.json` (`--no-learn` 可关;本技能**只改文件、不自动 push**)。 ## 授权依据(企业微信场景)— 已废弃 **2026-10-12 需求变更:授权字段已删除,默认全部授权。** 本节仅作历史记录保留。 原规则(不再执行):企微会话里系统提示「对方默认同意存档会话内容」时,该截图可作为授权的依据;没有该提示一律按 `pending` 处理。 现在采集不再需要任何授权参数;但**企微「默认同意存档」以外的会话**(如外部微信群截图)仍要注意:授权虽不再是闸门,素材来源合规则仍是运营责任,落库时在 `summary` / `sourceType` 里注明来源(如 `wx-group`),便于事后追溯。 ## 采集素材上传:必须落「看板同源」路径 案例检索侧只看 `materialAssets[].url`。**且该 URL 必须能被看板前端 `fetch()` 到**,否则「复制图片」功能会静默失败。两条硬约束: 1. **同源**:`s3.fmode.cn` **不返回 `Access-Control-Allow-Origin`**,跨域 `fetch` / canvas 一律失败(浏览器只报 `Failed to fetch`,无 CORS 报错)。所以素材应上传到**项目自身的 OBS 前缀** `dev//`,通过网关 `https://app.fmode.cn/dev//` 读取(已验证同源可 fetch、可进剪贴板)。 2. **上传目标 = `dev//`,不加 `browser/` 段**:网关 `/dev//` 直接映射 OBS `dev//`。传成 `dev//browser/` 会让网关回退到 SPA index.html(返回 200 但内容是 HTML)。 旧做法(s3.fmode.cn 分享链接)仍可用于**报告类**交付物,但不要用于需要前端二次读取的案例素材。 ## 素材上传(`--upload`,默认开) 案例检索侧只看 `materialAssets[].url`,本地路径不算数。**ingest 现在会真正上传本地素材**, 拿到公网 URL 再写进案例包——不再依赖人工先传一遍。 URL 优先级(`resolveAssetUrl`): | 顺序 | 条件 | 结果 | | --- | --- | --- | | ① | `item.url` 已存在(文档/清单里带来的远程 URL) | 原样使用,**不触发上传** | | ② | 本地文件且 `--upload` 开(默认) | 调 `uploadMaterial()` 真上传,拿公网 URL | | ③ | ② 失败 / 关闭时的显式回退 | `--asset-base-url <前缀>` 派生(旧行为,标记 `isFallbackUrl`) | | ④ | 都没有 | `url` 留空 + `warnings` 明确提示(**绝不伪造 URL**) | **通道**:走兄弟技能 `fmode-storage` 的**第 0 级通道**(云函数预签名直传,免密钥)。 容器只需 `FMODE_SESSION_TOKEN`,真实 AK/SK 始终留在云函数侧;**技能代码里绝不内联任何对象存储凭据**, 也**不回落**本地 AK/SK 通道(宁可在 ingest 里降级留空 URL)。 **object key 规范**(相对 key,云函数会自动补 `user/<调用者id>/` 前缀,**别自己加**): ``` case-library//-. # 例:case-library/20261010/imperial-aero50006-1.png # → https://s3.fmode.cn/user//case-library/20261010/imperial-aero50006-1.png ``` `caseSlug` 取 `--source-ref`,无则取 `--title`,再无则 `case`;`order` 与素材 `order` 一致(1 起)。 幂等:同一 `(localPath, key)` 在**本进程内**只传一次(Map 缓存),重复调用直接复用回执。 **失败降级(绝不拖垮整批 ingest)**:单张上传失败 → 该素材 `url` 留空、 push 一条 `上传失败 :<原因>;该素材将无公网 URL`,其余素材照常上传, 最后再汇总一条 `N 张上传失败`。若原因是兄弟技能缺失/不可用,汇总里会点明「fmode-storage 不可用」。 **上传失败不会让 ingest 崩**,案例包照常产出(合规红线字段不变:`pending` / `readyForUse=false`)。 **什么时候用 `--no-upload`**: - 离线自测 / 单测(不想因为环境差异联网); - 素材已经在别处传过、只想用 `--asset-base-url` 派生; - 环境确实装不了 `fmode-storage`,又接受「url 留空、稍后由归档方补齐」。 ```bash # 默认:真上传(推荐) node scripts/case-intake.mjs ingest --image ./img.jpg --authorization authorized # 离线:不上传,只按前缀派生 node scripts/case-intake.mjs ingest --image ./img.jpg --no-upload \ --asset-base-url https://s3.fmode.cn/user//case-library/20261010 ``` 单独传一张排障(也是官方 CLI): ```bash node scripts/storage-bridge.mjs --file ./img.png --case-slug imperial-aero50006 --order 1 # → { "url": "https://s3.fmode.cn/user//case-library//imperial-aero50006-1.png", "key": "...", "bytes": 12345 } ``` 上传后务必用匿名 GET 复核公网可读(`200 image/png` 且字节数一致)——签名与 Content-Type 绑定,只看上传回执不够。 `case-package.json` 里 `materialAssets[].uploadKey / uploadBytes / uploadVia` 记录了上传回执,便于追溯。 > `fmode-storage` 未安装时,`uploadMaterial` 抛 `StorageUnavailableError`(明确错误码 `STORAGE_UNAVAILABLE`), > 不会静默失败。安装:`npx fmode-storage@latest install`;或用环境变量 `CASE_STORAGE_SCRIPT` 指向其 `uploader.mjs`。 ## 打标:学校不在白名单时显式传 --school-canonical `references/tag-dictionary.json` 的 `dimensions.schoolCanonical.values` 是**有限白名单**(约 33 所,多为 QS 百强英/澳/美/港/新名校),**不含西交利物浦大学**等中外合作办学主体;模型也不会把「西浦」直接归一到未收录的书写名。 所以遇到白名单外的学校(如西浦 XJTLU、宁波诺丁汉、昆山杜克),**采集时显式传** `--school-canonical <标准中文书写名>`,并同时补 `--country`;否则 `schoolCanonical` 会留空、检索必然漏检。事后建议把该校补进字典白名单。 ## 合规判断(能力 5) | 判定 | 落到哪 | | --- | --- | | 授权缺失 / pending / denied | `riskFlags: UNAUTHORIZED` + blocker,**不入库** | | 客诉 / 负面事件(投诉、退费纠纷、挂科、维权…) | `riskFlags: COMPLAINT` / `NEGATIVE_EVENT` + blocker,**不得对外展示** | | 隐私片段(手机号 / 邮箱 / 微信号 / 姓名…) | `privacyFindings[{field, rule, snippet}]`,snippet **已掩码** | | 与留学/课程无关 | `riskFlags: OFF_TOPIC` | | 无任何素材 | blocker,无法构成案例 | `complianceBlockers` 非空 → `submit` **拒绝提交**(即使授权通过)。 ### 掩码只作用于 privacyFindings —— 别把 PII 漏在别处 自动掩码**只**改 `privacyFindings[].snippet`;`title` / `summary` / `imageText` / `materialAssets[].ocrText|description` 都可能是**原样**文本。而 `title` 默认取 OCR 首行——**微信群名常直接含学员全名**(如「S26 101-ECO414某某某-(7)」),等于把姓名写进标题。 提交前**逐字段**扫一遍这五个位置:`title` / `summary` / `imageText` / `materialAssets[].ocrText` / `materialAssets[].description`,把姓名、顾问昵称、机构名替换成占位(`贾**` / `顾问` / `创*留学`)。复核命令:整包 `json.dumps` 后查关键姓名子串是否为 0。 另外「微信**群聊**截图」不是企微「默认同意存档」会话,图里也没有该告知 → 授权按 `pending` 处理,**必须由用户明确确认**才能 `--authorization authorized`。 ## 归档通道(双通道,自动选择) `submit` 会**先试云函数,不可达时自动回落 Parse 直写**,两条通道落库字段逐字一致(都只进待审清单): | 通道 | 何时用 | 说明 | | --- | --- | --- | | 云函数 `caseSubmit` | 部署环境挂载了函数运行时 | `POST /api/functions`,`{path,params,token}`;返回 `{ok:true,result:{...}}` | | **Parse 直写**(本部署实际通道) | 云函数端点不可用(本部署:客户自有云未挂载函数运行时) | 直接写 `CaseAsset`,字段与 `caseSubmit.js` 对齐;`--no-direct` 可禁用回落 | - 本部署(`server.lumistedu.com`)**没有可用的函数运行时**:`/api/functions` 只是定位器端点、`/parse/functions/*` 只有 Parse 原生注册表。项目的真实数据架构就是浏览器端 Parse 直连(见 `web/src/app/core/data.service.ts`),归档走同一路径。 - **仍受同一套红线约束**:`reviewStatus='pending'`、`readyForUse=false`、`tenantId + idempotencyKey` 幂等 —— 新案例只进待审清单,不进公共素材库。 - `submit` 必然以 `fallback: 'cloud-unavailable -> parse-direct'` 返回,**这是本部署的正常路径、不是故障**:不要为了"修好云函数"去反复调 `/api/functions`(任何 path 都 500,连通性探针也一样)。 - 通道自检:`node scripts/case-intake.mjs probe`(只读,应返回 `ok:true, hasMasterKey:true`)。 - 服务端特权密钥只从环境变量 `CASE_PARSE_MASTER_KEY` 或 0600 的 `~/.fmode/config/enterprise/current.json` 读取,只进请求头,**不落盘、不打印**。 - 线上 `CaseAsset` 字段类型:`targetCustomer` / `resultEvidence` / `outcome` / `groupInfo` 是 **Object**(不是字符串),本通道已按此对齐。 ### 落库字段映射(易错,改过就回读) - `fitStatus`(适用跟进状态,见 PRD `10-案例推荐策略.md`)是**前端真正读的字段**(`case-list` / `case-detail` 的 `item.fitStatus`)。案例包里按契约是 `fitStatus[]` **数组**,线上字段是**单值字符串** → 取首个写入。**漏写 = 前端永远显示默认「新进线」、按跟进状态筛选永远不匹配。** - 「出分反馈/家长认可/退费保障」类案例对应 **异议处理中 / 沉默待跟进**(不是「已成交」);打标前先查 PRD 策略表,别凭直觉。 - `reviewHint` 等只读知会字段一并写入。 - 提交后**必做字段回读**:按 `tenantId + idempotencyKey` 查回该条,逐字段比对(尤其 `reviewStatus` / `readyForUse` / `fitStatus` / `maskingStatus`),**别只看 `submitted:true`**。直写通道对未知字段是静默忽略的(Parse REST 会剥掉 schema 外的列),字段名写错不会报错、只会静默丢。 ## 云函数(部署若挂载了函数运行时) | 函数 | 用途 | | --- | --- | | `caseSubmit` | 提交案例;写 `reviewStatus='pending'`、`readyForUse=false`;`tenantId + idempotencyKey` 幂等 | | `casePendingList` | 查待审清单 | | `caseApprove` | 负责人审核放行(`approve` → `readyForUse=true`) | | `caseSearch` | 检索(只返回 `reviewStatus='approved'` 且 `readyForUse=true`) | `tenantId` 由云函数从**服务端会话上下文**解析,客户端不传。 ## 安装为 FmodeCode / Claude Code 技能 ```bash npx --yes skill-case-get@latest workspace # 项目级 → ./.claude/skills/skill-case-get npx --yes skill-case-get@latest install # 用户级 → ~/.claude/skills/skill-case-get ``` 安装后可直接提示:`这 9 张图是同一个案例素材,帮我拆解、打标,然后归档到待审清单。` ## 注意事项 - `.docx` / `.pptx` 解包走 python3 标准库,**不要**为此引入 python-docx / python-pptx / 任何 npm 包。 - 本地素材默认**真上传**(`--upload` 开,走 fmode-storage 第 0 级免密钥通道)拿公网 URL;`--no-upload` 才回落到 `--asset-base-url` 派生。两种情况下都**不伪造 URL**,拿不到就留空并在 `warnings` 里说清楚。 - 视频抽帧是均匀取点(默认 6 帧,`--frame-count` 调整);转写走网关按音频时长计费。 - 枚举统一**小写**(对齐线上:`pending` / `authorized`),展示层再做中文化。注意:**授权类枚举已随授权字段一起废弃**;案例类型枚举是中文单值(`对外案例`/`买课案例`/`高分案例`/`提分案例`/`录取案例`/`申诉案例`/`内部学习`)。 ## 看板侧字段(消费端,容易被采集侧忽略) - **`caseType`(案例类型,单值中文)**:区分「对外可用」与「销售内部学习」。 - 对外:`对外案例` / `买课案例` / `高分案例` / `提分案例` / `录取案例` / `申诉案例` - 内部:`内部学习` —— **默认不出现在检索结果**,与对外案例严格分开(用户明确要求:长截图类素材是给销售内部学习用的)。 - 存量回填有据可依:由 `highlightType`(亮点)映射,`录取→录取案例`、`申诉成功→申诉案例`、`提分→提分案例`;**映射不上的留空并交人工定,不凭空猜**。 - **`followUpScript`(跟进话术)**:看板「复制图片+话术」按钮读这个单值字段,采集时要写进去,否则复制出来只有图没有话术。 ## 一键复制图片:浏览器剪贴板的三条硬约束 看板支持销售「复制图片 / 复制图片+话术」直接粘进企业微信。实现时踩过三个坑,**任何批量交付图片的界面都会重现**: 1. **必须用 `ClipboardItem` 写图片,不能用 `writeText`** —— 纯文字通道复制过去只有文字,销售要的是能直接粘的图。 2. **`clipboard.write()` 必须在用户手势的同步栈里调用**。先 `await fetch(...)` 再 write,临时授权已过期 → `NotAllowedError`。正确姿势:`new ClipboardItem({'image/png': fetchPng(url)})` 传 Promise,write 同步发出。 3. **Chromium 剪贴板只接受 PNG**:写 `image/jpeg` 抛 `NotAllowedError: Type image/jpeg not supported on write`。所以 `.jpg` 素材必须先经 `createImageBitmap` + `canvas.toBlob('image/png')` 转码(**转码放在 Promise 内部**,不破坏第 2 条)。 验证方法:CDP `Browser.grantPermissions(origin, ['clipboardReadWrite','clipboardSanitizedWrite'])` + `Runtime.evaluate(..., userGesture=True)`,再 `navigator.clipboard.read()` 回读确认 item 的 type 是 `image/png`、size > 0。仅凭 console 无报错不算通过。 ## 文档 → 话术库(ScriptAsset):把官方销售话术文档接进案例库 **触发**:用户给一份飞书/Word 话术文档,要求「存档进案例库 + 建话术库 + 支持归档时提取」。 **流程**(每步都要真实回读,别信中间回执): 1. **读原文**:`lark-cli docs +fetch --doc --doc-format markdown > /tmp/x.md`。输出是 **JSON 包一层**,内容在 `data.document.content`(直接当 markdown 当心被截断;重定向落盘再解析)。 2. **抽图并下载**:markdown 里图片是 `https://feishu.cn/file/`。**下载端点是 `GET /open-apis/drive/v1/medias//download`**(`drive +download` 走不通,会 403/127)。图片 alt 文本已是图片描述,直接当 `imageText` 用,无需再过视觉模型。 3. **结构化解析**:`## Q:<问题>` 抽出问题;bullet 行(`- A:…`)是话术要点数组。**注意文档自身的层级缺陷**:有的章节误用 `##` 而非 `#`(如「7)公司相关」),会被并进上一章 —— 解析前先按标题名正则修正层级;另有 `1. **问题:**答案` 式条目,加粗可能把正文裹进来,要按 `:` 或首个 `。` 切开。 4. **素材落同源**:图片必须上传到**看板同源** `dev//`(`fmode-storage` 第 3 级项目 STS),否则 `s3.fmode.cn` 无 CORS,前端 `fetch` 取不到图、复制图片必失败。文件名 `NNN.`(按魔数嗅探真实类型,别信扩展名)。 5. **写库**:`ScriptAsset`(与 `CaseAsset` 同租户),字段:`question` / `category`(章节) / `chapter` / `answerPoints`(数组) / `images`(同源 URL 数组) / `imageText` / `fitStatus`(适用跟进状态) / `sourceDocId`+`sourceDocTitle`+`sourceRef`(可溯源)`/ order`。幂等键 = `tenantId + idempotencyKey`(由 `文档ID + 分类 + 问题` 派生),重复导入不产生重复条目。 6. **写后回读**:`curl` 匿名 GET 同源图(200 + md5 同源),Parse 侧按 `idempotencyKey` 计数并抽条比对。 **消费端两个入口(都要做,缺一不可)**: - **话术库页**(左侧独立栏位 `/scripts`):**列表主体是「问题」**,每项聚合该问题的话术要点 + 配图;检索维度必须与案例库对齐(分类 / 跟进状态 / 关键词,关键词要能命中的**话术正文**,不只标题)。 - **案例归档「从话术库选取」**:抽成共享组件(`ScriptPickerComponent`),在**案例列表编辑**与**案例详情**两处复用;`ViewChild` 调 `openPicker()`,`(pickText)` 回填 `followUpScript`。列表页回填=**覆盖**(方便重挑),详情页=**追加**(不丢已有内容)。 **坑**:编辑表单可能在**两个地方**(列表页内嵌 + 详情页),只改一处会出现「详情页有、列表页没有」;Angular 懒加载 chunk 名字哈希后会变,浏览器可能仍缓存旧 chunk —— 验证前先 `Network.setCacheDisabled` + `?v=`。