This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/packages/activity-sdk/src/view.ts
T
ChaiHaoandClaude Opus 5 374206b408 六个活动的汇报界面与技能补齐;skill-engine 接上装配面
汇报界面(AM 07):
  · 视图外壳与卡片件提到 `@sdk`(viewShell / kvCard / tableCard / stagesCard / noteCard)——
    六个活动共用一套面貌,不各自发明;样式内联自带(沙箱页面不继承工作台 CSS)。
  · flow 四活动按**步序位置**挂模版:
    A1 事态全景 / 打标判据 / 成果预览;A4 取价基准 / 定价与门禁 / 草案待放行;
    A6 正式报价单 / 放行即发出;A7 结果登记 / 闭环待放行。
  · loop 两活动按**阶段与里程碑**组织:案例全景一屏,画阶段生命周期态、当下可为、目标缺口 ——
    素材全部来自语义引擎,零 LLM 即可成屏;模型只决定下一步做什么,不负责画面。
    为此给 loop-engine 补了视图端口,与 flow 共用同一个网关。
  · 每张模版都把「这一屏在说什么 / 放行意味着什么」写在数据旁边 ——
    人要在数据上作判断,判据就得摆在手边。

技能(AM 08):
  · 六个活动各写了 1–2 条真技能:适用描述 + 方法正文 + 结晶片段脚本。
    正文写的是**方法经验**而非流程复述(如「原文没写的不要补」「该等还是该问」
    「无基准不是异常,是一种基准」)。
  · 资源声明放**技能 ID**(权力面),资产放**定义本体**(方法面),两处分清。
  · **skill-engine 从来没有被构造过** —— 模块实现完整却无人调用,这是它一直没被用上的原因。
    容器里补上:目录与白名单都取自活动清单,装配前查扩权。
  · loop 的叙事面接入**技能目录**(渐进披露:常驻上下文只给「有哪些方法、何时适用」,
    正文等 AI 说要才加载),装配随即**盖戳入任务记忆** —— 回放与归因依赖之。

gates: tsc / eslint / depcruise(334 模块)/ 423 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-27 10:59:13 +08:00

114 lines
5.4 KiB
TypeScript

/**
* 视图模版工具箱 —— 活动开发者写「呈现」时看到的世界(AM 07)。
*
* 模版是**内容不是代码**:这里产出的一律是无脚本、无事件处理器、无外链的静态 HTML;
* 数据由基座在推送时填(`data-slot` 取值、`data-rows` 逐条展开)。
* 容器是沙箱 iframe(`sandbox=""`),因此**样式必须自带** —— 页面不继承工作台的 CSS。
*
* 六个活动共用这一套外壳:汇报面貌一致,不是每个活动各自发明一套。
*/
export type Tone = 'brand' | 'ok' | 'warn' | 'danger' | 'case' | 'archive';
const ACCENT: Readonly<Record<Tone, string>> = {
brand: '#3370ff',
ok: '#0aa294',
warn: '#ff8800',
danger: '#e5484d',
case: '#7f3bf5',
archive: '#46586a',
};
/** 一套克制的排版;所有尺寸用相对单位,窄栏也不横向溢出 */
const BASE_CSS = (accent: string): string => `
:root { color-scheme: light; }
* { box-sizing: border-box; }
body {
margin: 0; padding: 18px;
font: 13px/1.6 -apple-system, "PingFang SC", "Noto Sans SC", "Segoe UI", sans-serif;
color: #1f2933; background: #f7f8fa;
}
.wrap { max-width: 760px; margin: 0 auto; }
.hd { display: flex; align-items: stretch; gap: 12px; margin-bottom: 14px; }
.hd .bar { width: 4px; border-radius: 2px; background: ${accent}; flex: none; }
.hd .kick { font-size: 11px; letter-spacing: .08em; color: #6a7686; }
.hd h1 { margin: 2px 0 0; font-size: 17px; font-weight: 650; letter-spacing: .01em; }
.card { background: #fff; border: 1px solid #e6e9ee; border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; }
.card h2 { margin: 0 0 10px; font-size: 12px; font-weight: 600; color: #6a7686; letter-spacing: .04em; }
.kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px 18px; }
.kv > div { min-width: 0; }
.kv em { display: block; font-style: normal; font-size: 11px; color: #8b95a3; margin-bottom: 2px; }
.kv b { font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }
.tbl-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; font-weight: 600; color: #6a7686; padding: 6px 8px; border-bottom: 1px solid #e6e9ee; white-space: nowrap; }
td { padding: 7px 8px; border-bottom: 1px solid #f0f2f5; overflow-wrap: anywhere; }
tr:last-child td { border-bottom: 0; }
.mono { font-family: "SF Mono", Menlo, Consolas, monospace; }
.tag { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600;
background: ${accent}1a; color: ${accent}; }
.note { font-size: 11px; color: #8b95a3; margin-top: 10px; line-height: 1.75; }
.steps { list-style: none; margin: 0; padding: 0; }
.steps li { position: relative; padding: 0 0 12px 20px; border-left: 2px solid #e6e9ee; }
.steps li:last-child { padding-bottom: 0; border-left-color: transparent; }
.steps li::before { content: ""; position: absolute; left: -5px; top: 5px; width: 8px; height: 8px;
border-radius: 50%; background: ${accent}; }
.steps .t { font-weight: 600; }
.steps .d { color: #8b95a3; font-size: 11.5px; }
`;
/** 页头 + 卡片区 —— 每张模版都长这样,只换标题与内容 */
export function viewShell(input: {
title: string;
kicker: string;
tone: Tone;
body: string;
}): string {
const accent = ACCENT[input.tone];
return `<style>${BASE_CSS(accent)}</style>
<div class="wrap">
<div class="hd"><span class="bar"></span><div><div class="kick">${input.kicker}</div><h1>${input.title}</h1></div></div>
${input.body}
</div>`;
}
/** 键值卡:`[标签, 取值路径]`;`mono` 用等宽显示编号类字段 */
export function kvCard(title: string, items: ReadonlyArray<readonly [string, string, ('mono' | 'tag')?]>): string {
const cells = items
.map(([label, path, style]) => {
const cls = style === undefined ? '' : ` class="${style === 'mono' ? 'mono' : 'tag'}"`;
return `<div><em>${label}</em><b${cls} data-slot="${path}"></b></div>`;
})
.join('');
return `<div class="card"><h2>${title}</h2><div class="kv">${cells}</div></div>`;
}
/** 逐条表:行数不写死,由 `data-rows` 展开 */
export function tableCard(
title: string,
rowsPath: string,
columns: ReadonlyArray<readonly [string, string, ('mono')?]>,
note?: string,
): string {
const head = columns.map(([label]) => `<th>${label}</th>`).join('');
const cells = columns
.map(([, path, style]) => `<td${style === 'mono' ? ' class="mono"' : ''} data-slot="${path}"></td>`)
.join('');
return `<div class="card"><h2>${title}</h2><div class="tbl-wrap"><table>
<thead><tr>${head}</tr></thead>
<tbody data-rows="${rowsPath}"><tr>${cells}</tr></tbody>
</table></div>${note === undefined ? '' : `<p class="note">${note}</p>`}</div>`;
}
/** 说明卡:把「为什么这么做」摆在数据旁边 —— 人要在数据上作判断 */
export function noteCard(title: string, html: string): string {
return `<div class="card"><h2>${title}</h2><p class="note" style="margin-top:0">${html}</p></div>`;
}
/** 阶段时间线:loop 按案例模型的阶段与里程碑组织汇报(AM 07) */
export function stagesCard(title: string, rowsPath: string, note?: string): string {
return `<div class="card"><h2>${title}</h2><ul class="steps" data-rows="${rowsPath}">
<li><span class="t" data-slot="name"></span><span class="d" data-slot="state"></span></li>
</ul>${note === undefined ? '' : `<p class="note">${note}</p>`}</div>`;
}