/** * 活动包清单 —— I01 品牌形象与销售工具包管理(案例活动)。 * * 业务出处:`docs/SS01-客户与市场/i01-html/I01-活动流程与工单说明.md`(7 步业务链条, * 工单只针对生产流程①→⑤,收成 1 张执行工单 + 2 张验收工单)、 * `docs/SS01-客户与市场/Demo-E04-I01-Activity台账关联与设计.html`(L-01 宏观市场台账 / * L-05 品牌内容库的字段表与读写关系)。 * * 这是本仓库第二个不属于 `quote-*` A1-A7 报价管线的活动,方法论照抄第一个 * (`crm-relationship-coverage` / E04)已验证过的样子:双级下拉入参、活动自有工作台账 * + 共享主数据、decide 决策卡循环做中间验收、六步案例模型。 * * 两处与 E04 的刻意不同: * ① E04 核心是**读写**共享主数据(`customer.profile`/`customer.contact`), * I01 核心是**只读** L-01(`market.signal`)、**读写**它独有的 L-05 * (`content.asset`)——本 Demo 范围内 I01 是 `content.asset` 唯一的读写者, * 不追加列合并,是这份活动包**第一次**完整声明这两张台账的物理结构。 * ② I01 没有 E04 那种「AI 做不到、必须人去线下执行」的环节(内容初稿由 AI 草拟, * 人只审不做)——所以只有 1 张执行工单(S2_requirement_confirm), * 验收工单反倒有 2 张(S4_draft_review 审初稿 + T_release 发布确认), * 与 E04(1 张验收决策 + 1 张终局放行)结构同型,只是执行工单少一张。 */ import type { ActivityManifest, DmnModel } from '@contracts'; import { activityIdSchema } from '@contracts'; import { CONTENT_SKILLS } from './skills.ts'; import { CMMN_XML, NARRATIVE, CONTENT_PHASES, CONTENT_STEPS } from './case-model.ts'; import { REQUIREMENT_LIST, DRAFT_REVIEW, PUBLISH_CONFIRM } from './views/report.ts'; const D = 'content_brand'; const ACTIVITY_ID = activityIdSchema.parse('brand-content-toolkit'); /** 没有判定表 —— 触发事件→内容类型、BT 层级→内容结构都是文字规则,不是离散分支命中表。 */ const EMPTY_DMN: DmnModel = { drd: [], tables: [] }; export const manifest: ActivityManifest = { identity: { activityId: ACTIVITY_ID, name: 'I01 · 品牌形象与销售工具包管理', packageVersion: '1.2.2', category: '内容生产', sdkVersion: '0.1.0-m5', checksum: 'dev-checksum-brand-content-toolkit', }, /* ── 契约七件 ────────────────────────────────────────────────── */ contract: { activityId: ACTIVITY_ID, version: '1.0.0', input: { /* * 不是来件解析(没有唯一「这封信」),是触发参数 —— 与 E04 同一处置 * (两级下拉 + 选填客户/说明): * ① `trigger` 必填 —— 主动 / 被动,显式下拉,不接受省略; * ② `reason`/`requester` 选填,只在对应 `trigger` 值时现身(`showWhen`); * ③ `targetCustomer` 选填字符串 —— 定向客户编码,通用内容留空。 */ data: { type: 'object', required: ['trigger'], properties: { trigger: { type: 'string', enum: ['proactive', 'reactive'], enumNames: ['主动触发(战略调整/新市场/工具过期/月度更新)', '被动触发(使用方请求)'], title: '触发方式', }, reason: { type: 'string', enum: ['strategy_shift', 'new_market', 'tool_outdated', 'monthly_update'], enumNames: ['战略调整', '进入新市场', '工具过期', '月度定期内容更新'], title: '主动触发的原因(仅主动触发时选)', showWhen: { field: 'trigger', equals: 'proactive' }, }, requester: { type: 'string', enum: ['E04', 'T03', 'T04', 'E03'], enumNames: [ 'E04 关系覆盖需要价值传递素材', 'T03 触达需要材料', 'T04 攻坚需要定制内容', 'E03 方案需要支撑', ], title: '被动触发的使用方(仅被动触发时选)', showWhen: { field: 'trigger', equals: 'reactive' }, }, targetCustomer: { type: 'string', title: '定向客户编码(选填,为特定客户定制素材时填;通用内容留空)', examples: ['CU-0142'], }, note: { type: 'string', title: '本轮说明(选填,背景信息 —— 帮AI判断内容侧重与紧迫程度)', examples: ['进入俄罗斯市场,需要一份中俄双语的公司介绍与能力声明,月底展会要用'], }, }, }, messageAllowed: true, messageHint: '内容运营可在此说明本轮生产的特殊要求、目标受众、或需要重点突出的内容侧重', }, output: { /* 单一必发口,无分支:I01 生产流程线性(①→②③→④→⑤),没有岔路口。 */ mandatory: [ { name: 'content-published', description: '本轮已发布的内容资产 —— 供 E04/T03/T04/E03 分发调用;当前代码库尚未实现这些下游活动的读取消费者,此口暂无消费者,仅按"载荷=已落地事实"的契约纪律预先声明', payload: { type: 'object', required: ['roundRef', 'publishedContentTotal'], properties: { roundRef: { type: 'string', title: '本轮案号' }, publishedContentTotal: { type: 'integer', title: '本轮已发布内容份数' }, }, }, }, ], selective: [], }, /* * 三份必交付物,逐一对应三张工单产出的成果。全部 `editableAtRelease: []`—— * 业务说明明文"验收工单只读展示,不可修改"(内容初稿与拟发布明细都是 AI 的结论, * 人只做通过/驳回或签字,不在验收页上就地改)。 */ requiredDeliverables: [ { key: 'requirement-confirm', label: '内容需求清单', ledger: `${D}.requirement`, ledgers: [`${D}.requirement`, `${D}.verdict`], editableAtRelease: [], }, { key: 'draft-review', label: '内容初稿', ledger: 'content.asset', editableAtRelease: [] }, { key: 'publish-confirm', label: '拟发布内容', ledger: 'content.asset', editableAtRelease: [] }, ], /* * 验收判据 —— **机检(L1/L2)只读基座数得出来的数**:`_rows`/`_missing`/按值计数基座现数, * 与执行卡的提交闸同一个数同一套算法。L4 三条是纯裁量(版本状态对不对、标签齐不齐、 * 多语言够不够)——机器数得出「有没有值」,数不出「对不对、够不够」,那是人对结果负责的地方。 */ acceptanceCriteria: [ /* * 两条 L1 的「人过了没有」一半靠记账旗(只有人回卡之后模型才记 —— 与 A1 的 * confirmed_total 同款,规范 §5.3-4 背书的人环凭据);但**每条都必须再钉一半 * 基座数**(2026-08-23 照 E04 的账整改):旗是模型写的,单独成谓词就是 * 「机检只读基座数」第 N 次违反的伪装形 —— E04 那单抢跑写旗、活单熔断, * 正是踩着这半边过的门。 */ { key: 'requirement-confirmed', label: '内容需求已经内容运营核对确认(执行工单已交回,清单有行且内容类型齐)', level: 'L1', predicate: `requirement_confirmed_total > 0 and ${D}_requirement_rows > 0 and ${D}_requirement_missing = 0`, }, { key: 'draft-approved', label: '内容初稿已经 AM Lead 审核通过(验收工单1已过,不处在驳回待改状态)', level: 'L1', predicate: 'draft_approved_total > 0 and content_asset_rows > 0 and content_asset_missing = 0', }, { key: 'draft-complete', label: '内容初稿逐份完整:标题、内容类型、语言版本、状态都有', level: 'L2', predicate: 'content_asset_rows > 0 and content_asset_missing = 0', }, /* * **资料文件真产出了没有** —— 两边都是基座现数:每一行都报了一种格式 * (四档计数之和 = 行数),而正文空着的行会被 `_missing` 数出来(body_md 是必填)。 * 从前这一步只产出一行标题,人点不开也下不来 —— 判据得能指向一个看得见的东西: * 这一条为真,件上每个资料名都点得开。 */ { key: 'asset-file-ready', label: '每份内容都产出了资料文件(格式已定、正文已写,件上点资料名即可打开)', level: 'L2', predicate: 'content_asset_rows > 0 and content_asset_missing = 0 and ' + 'content_asset_file_format_md + content_asset_file_format_html + ' + 'content_asset_file_format_docx + content_asset_file_format_xlsx = content_asset_rows', }, { key: 'publish-registered', label: '拟发布内容已登记为已发布状态且逐项完整', level: 'L2', predicate: 'content_asset_content_status_published > 0 and content_asset_missing = 0', }, { key: 'version-status-correct', label: '版本号与状态是否正确:版本号已更新、状态流转无误', level: 'L4' }, { key: 'scene-tag-complete', label: '场景与 BT 标签是否登记完整', level: 'L4' }, { key: 'language-complete', label: '多语言版本是否齐全', level: 'L4' }, /* L5 是责任关:那张卡本身就是这道关,卡面上不摆它(基座统一滤) */ { key: 'owner-release', label: '发布入库放行:签下去即正式生效并写入 L-05', level: 'L5' }, ], revisableAtRelease: [], prohibitions: [ { key: 'no-fabricate-signal', statement: '市场信号读不到相关内容时不得编造 —— 如实基于已有资产与客户信息拟定' }, { key: 'no-skip-review', statement: '内容状态流转不得跳步 —— draft → reviewed → published 每一步都要真的写过' }, { key: 'no-external-write', statement: '放行前不得触达外部系统 —— 触达世界的动作集中在放行那一刻' }, ], authorization: { discretion: [], preauthorizedActions: [], /* 基线三次(S2/S4通过/T_release)+ 决策卡循环预留两轮(每轮驳回+重提交各算一次,+4),留够余量 */ humanCallBudget: 10, }, resources: { tools: [], skills: CONTENT_SKILLS.map((s) => s.id), models: ['deepseek-v4-flash'], roles: ['AM Lead'], budget: { steps: 60, tokens: 400_000, activeMinutes: 60, humanCalls: 10 }, }, }, /* ── 声明 ────────────────────────────────────────────────────── */ declarations: { models: ['deepseek-v4-flash'], tools: [], ledger: [ /* * ── L-01 宏观市场台账(kind:'master',I01 只读)── * 首次完整声明:本仓库尚无其它活动读写它,不是追加列。 */ { domain: 'market', object: 'signal', kind: 'master', label: '宏观市场台账', fields: [ { name: 'signal_id', type: 'text', primaryKey: true, masked: false, label: '信号ID' }, { name: 'signal_type', type: 'text', primaryKey: false, masked: false, tally: ['macro_economy', 'industry_trend', 'competition', 'policy', 'customer_market'], label: '信号类型', }, { name: 'title', type: 'text', primaryKey: false, masked: false, label: '标题' }, { name: 'content', type: 'text', primaryKey: false, masked: false, label: '详情' }, { name: 'source', type: 'text', primaryKey: false, masked: false, label: '信号来源' }, { name: 'signal_date', type: 'date', primaryKey: false, masked: false, label: '信号日期' }, { name: 'market_region', type: 'text', primaryKey: false, masked: false, label: '所属市场' }, { name: 'industry', type: 'text', primaryKey: false, masked: false, label: '细分行业' }, { name: 'chip_category', type: 'text', primaryKey: false, masked: false, label: '关联芯片品类' }, { name: 'importance', type: 'integer', primaryKey: false, masked: false, label: '重要性评分(1-5)' }, { name: 'impact_dir', type: 'text', primaryKey: false, masked: false, tally: ['opportunity', 'threat', 'neutral'], label: '影响方向', }, { name: 'signal_status', type: 'text', primaryKey: false, masked: false, tally: ['pending', 'evaluated', 'archived', 'expired'], label: '信号状态', }, { name: 'version', type: 'text', primaryKey: false, masked: false, label: '版本号' }, { name: 'review_note', type: 'text', primaryKey: false, masked: false, label: '复盘备注' }, ], }, /* * ── L-05 品牌内容库(kind:'master',I01 唯一读写者)── * 首次完整声明并持有全部基本列(不是追加,`declarations.ledger` 里明写 required/expect, * 因为没有别的活动会与 I01 在这张表上"谁先立谁的字段"打架)。 */ { domain: 'content', object: 'asset', kind: 'master', label: '品牌内容库', fields: [ { name: 'content_id', type: 'text', primaryKey: true, masked: false, label: '内容ID' }, { name: 'content_type', type: 'text', primaryKey: false, masked: false, required: true, label: '内容类型' }, { name: 'title', type: 'text', primaryKey: false, masked: false, required: true, label: '内容标题' }, { name: 'language', type: 'text', primaryKey: false, masked: false, required: true, label: '语言版本(多语言用" · "连接)', }, { name: 'version', type: 'text', primaryKey: false, masked: false, required: true, label: '版本号' }, /* * **发布那一刻才必填**(2026-08-23):S5 的剧本明写「发布前场景必须登记」, * 而声明只标 expect —— 机器数不出来,那句话就只能靠 L4 让人肉眼核。 * `requiredWhen` 把必填钉在**已发布**的那几行:草稿态不拦(那时还没定场景), * 一旦状态流到 published,红带/提交闸/放行门 L2 三处同源同数地拦。 */ { name: 'scene', type: 'text', primaryKey: false, masked: false, expect: true, requiredWhen: { column: 'content_status', value: 'published' }, label: '适用场景', }, /* * BT 标签 —— 目标件的验收工单1 就有这一列,而从前 `content.asset` 压根没有它: * L4 判据「场景与 BT 标签是否登记完整」让人核一个卡面上不存在的东西 * (判据指向的东西必须摆得出来,E04 那一课)。与 `scene` 同款:发布那一刻才必填。 * * **不带 tally** —— 与 `requirement.scene` 同一条教训(2026-08-21): * tally 的取值域得是 ASCII(`data-class-of` 只认 ASCII),而这一列在件上是 * 普通格子、走 `data-slot` 直显,没有翻译层 —— 存 `bt_03` 人看到的就是 `bt_03`。 * 存人读值("BT-01"/"BT-02"/"BT-03"),屏上与目标件一字不差。 */ { name: 'bt_tag', type: 'text', primaryKey: false, masked: false, expect: true, requiredWhen: { column: 'content_status', value: 'published' }, label: 'BT 层级标签', }, { name: 'content_status', type: 'text', primaryKey: false, masked: false, required: true, tally: ['draft', 'reviewed', 'published', 'deprecated'], label: '内容状态', }, { name: 'brand_tone', type: 'text', primaryKey: false, masked: false, expect: true, label: '品牌调性标签' }, { name: 'target_customer', type: 'text', primaryKey: false, masked: false, label: '定向客户版本' }, /* * ── 一份内容 = 一份**资料文件**(2026-08-23 用户:「审核资料这一步要真实 * 产出资料文件,格式可以是 word/excel/md/html,点资料名能打开渲染」)── * * 从前只有一句 `file_ref: '文件位置'`,而 S3 的剧本明写「本 MVP 阶段可留空」: * **这一步压根没产出过任何文件** —— 屏上一行标题,点不开、下不来, * 人要审的那份初稿根本不在系统里。 * * 现在正文落 `body_md`(Markdown 原文,AI 写的就是它),格式落 `file_format`, * 基座按 `file` 段现造成真字节(见 `apps/api/src/app/file-compose.ts`): * .docx 用 Word 打得开、.xlsx 用 Excel 打得开,屏上渲的就是那份字节读回来的样子。 * * 为什么正文只写一种(Markdown)而不是四种各写一遍:四份就是四份事实, * 改一处就分叉。**写一份,按格式投影** —— 与「文件是台账的投影」同一条。 */ { name: 'body_md', type: 'text', primaryKey: false, masked: false, required: true, label: '内容正文(Markdown 原文)', }, { name: 'file_format', type: 'text', primaryKey: false, masked: false, required: true, tally: ['md', 'html', 'docx', 'xlsx'], label: '资料文件格式', }, { name: 'file_ref', type: 'text', primaryKey: false, masked: false, label: '文件位置' }, { name: 'reviewer', type: 'text', primaryKey: false, masked: false, label: '审核人' }, ], /** * 这张表的每一行还是**一份资料文件**:标题、正文、格式各取哪一列。 * 这一段同时是授权面 —— 不声明它,件上的资料名就点不开(基座 403)。 */ file: { titleColumn: 'title', bodyColumn: 'body_md', formatColumn: 'file_format' }, }, /* ── I01 自有工作台账(kind:'record',caseKey:'round_ref') ── */ { domain: D, object: 'requirement', kind: 'record', caseKey: 'round_ref', label: '内容需求清单', fields: [ { name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '案号' }, { name: 'req_seq', type: 'integer', primaryKey: true, masked: false, label: '本轮需求序号' }, { name: 'content_type', type: 'text', primaryKey: false, masked: false, required: true, label: '内容类型' }, { name: 'source', type: 'text', primaryKey: false, masked: false, expect: true, label: '需求来源' }, { name: 'target_customer', type: 'text', primaryKey: false, masked: false, expect: true, label: '目标客户' }, /* * **不带 tally**(2026-08-21 实测逼出的修死):这一列是编辑面上的普通格子 * (`REQUIREMENT_TABLE` 里就是一个 ``,不是 `data-class-of` 徽标), * tally 过的 ASCII 码没有翻译层就直接摆上屏——实测人看见的是裸的 "solution", * 不是"方案"。`content.asset.scene`(S3 照抄这一列)本来就是自由文本,两处 * 现在口径一致:AI 写人读标签(首触/方案/攻坚/展会/报价),不写 ASCII 码。 */ { name: 'scene', type: 'text', primaryKey: false, masked: false, expect: true, label: '适用场景' }, { name: 'priority', type: 'text', primaryKey: false, masked: false, tally: ['high', 'medium', 'low'], label: '优先级', }, { name: 'origin', type: 'text', primaryKey: false, masked: false, label: '来源(ai/human-added)' }, ], }, { domain: D, object: 'verdict', kind: 'record', caseKey: 'round_ref', label: '本轮内容生产结论', fields: [ { name: 'round_ref', type: 'text', primaryKey: true, masked: false, label: '案号' }, { name: 'trigger', type: 'text', primaryKey: false, masked: false, label: '触发方式(人读标签,非入参英文码)' }, { name: 'trigger_detail', type: 'text', primaryKey: false, masked: false, label: '触发细节(人读标签)' }, { name: 'target_customer', type: 'text', primaryKey: false, masked: false, label: '定向客户' }, { name: 'note', type: 'text', primaryKey: false, masked: false, label: '本轮说明' }, { name: 'requirement_confirmed_total', type: 'integer', primaryKey: false, masked: false, label: '需求已确认' }, { name: 'draft_approved_total', type: 'integer', primaryKey: false, masked: false, label: '初稿已审核通过' }, { name: 'publish_prepared_total', type: 'integer', primaryKey: false, masked: false, label: '发布准备已收口' }, { name: 'content_produced_total', type: 'integer', primaryKey: false, masked: false, label: '本轮产出内容份数' }, { name: 'summary', type: 'text', primaryKey: false, masked: false, label: '本轮生产结论' }, ], }, ], listeners: [], harness: { memoryWindow: 24, /* 内容生产不是紧迫单,给够时间让人审核 */ stallAfter: 'PT4H', nudge: [{ after: 'PT2H', action: 'nudge' }], retry: { times: 2, backoff: 'PT30S' }, }, }, /* ── 资产初始版 ──────────────────────────────────────────────── */ assets: { businessModel: { cmmnXml: CMMN_XML, narrative: NARRATIVE, dmn: EMPTY_DMN, steps: CONTENT_STEPS, phases: CONTENT_PHASES, }, skills: CONTENT_SKILLS, /* * 三个局面各一张: * · `requirement-list` —— S1/S2 摆,内容需求清单,可编辑; * · `draft-review` —— S3/S4 摆,内容初稿,只读(业务说明明文"人只审不改"); * · `publish-confirm` —— S5/T_release 摆,拟发布内容,只读,放行后即归档件。 */ viewTemplates: [ { name: 'requirement-list', html: REQUIREMENT_LIST }, { name: 'draft-review', html: DRAFT_REVIEW }, { name: 'publish-confirm', html: PUBLISH_CONFIRM }, ], cardSchemas: {}, distillHooks: [ { on: 'terminal', prompt: '归纳本轮内容生产:需求识别依据、审核是否被驳回过(为什么)、市场依据来自哪些信号、发布的内容覆盖哪些场景与语言。', }, ], }, /* * ── 样例集 —— 三条形状真不同 ──────────────────────────────── * * **第一条必须是满载的那条**(2026-08-23,与 E04 同一课):测试台落到一个活动上 * 就自动铺 `samples[0]`(activity-run.tsx 的 useEffect),第一条就是人打开这一屏 * 看见的那份入参。从前第一条是月度更新、只有两个键,表单大半是空的,看着像没预填。 * **样例名一律 ≤14 字**:超了测试台的样例钮只显示「样例 N」,那等于没名字。 */ samples: [ { name: 'E04 请求定向素材', input: { trigger: 'reactive', requester: 'E04', targetCustomer: 'CU-0142', note: 'E04 关系覆盖与关键人经营需要一份面向 Itelma 的价值传递素材,用于下轮拜访', }, expect: { ports: ['content-published'] }, }, { name: '月度定期内容更新', input: { trigger: 'proactive', reason: 'monthly_update', note: '八月例行内容刷新:优先看版本最旧、最久未更新的那几份工具包', }, expect: { ports: ['content-published'] }, }, { name: '进入新市场 · 俄罗斯', input: { trigger: 'proactive', reason: 'new_market', note: '公司战略进入俄罗斯市场,需要一份中俄双语的公司介绍与能力声明,月底展会要用', }, expect: { ports: ['content-published'] }, }, /* * **一轮里三种形态各要一份** —— 这一条样例是为「资料文件」那一环常备的 * (2026-08-23):前三条要的都是成篇的文档,模型一律挑 docx 并不算错, * 于是表格型与网页型那两条路平时走不到。这一条把三种形态摆在同一封需求里: * 对照表是表(xlsx)、落地页是网页(html)、销售速记是短要点(md)。 */ { name: '对照表与落地页', input: { trigger: 'reactive', requester: 'FAE 华东', note: '客户要一份 TI 车规 MCU 的替代料参数对照表(逐型号列封装/温度/认证/交期,做成表格发过去);' + '同时官网要上一版车规 MCU 能力的落地页文案(网页直接看);' + '再给销售一页随身速记,三五条要点即可。', }, expect: { ports: ['content-published'] }, }, ], entry: { file: 'index.js', checksum: 'dev-entry-brand-content-toolkit' }, };