Archived
M5(完成): @activity-sdk 与首个活动包 quote-intake
@activity-sdk(§8.2 / ADR-004): - 基座六件接口(契约 / 交互 / 记忆 / 数据 / 工具 / LLM)—— 活动看到的全部世界 - defineActivity() 的 ABI;本地开发宿主(mock 工具表 + 模拟工位 + 录制夹具), 宿主自身就守三规矩:裸调用拒发、视图含脚本拒推、监听无时限拒登记 activities/quote-intake —— 统一报价 A1 · RFQ 需求分析(flow): - 契约七件 / 步序声明五步(LLM 抽取 → 条件跳步补全 → 查历史价 → DMN 打标 → 验收放行) - 三分类规则 P-03 落 DMN 决策表(C 优先于 A,命中行留痕) - 台账声明两对象、监听时限必配、SLA 落 Harness depcruise 实测拦下活动包越界 import(activities → apps 双向隔离), 测试遂按边界拆分:活动侧只用 @sdk/@contracts/@sfeel 358 个测试通过,门禁五道全绿 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
4ad901c452
commit
c6cdb4c310
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 本地开发宿主 —— mock 工具表 + 模拟工位(工单 / 卡)+ 回放夹具(编码规范 §8.2)。
|
||||
*
|
||||
* 「系统外对着声明开发(本地 mock)」:开发者不必起系统就能跑活动;
|
||||
* 夹具录下的出入参可直接喂给回放审(§7.3 两缝的同一副夹具)。
|
||||
*/
|
||||
import type { ActivityBase, ContractSpec } from './base.ts';
|
||||
import type { ActivityDefinition, ActivityOutcome } from './define.ts';
|
||||
import type { ChatCardDraft, LedgerKey, LedgerRow, ViewBlockLike, WorkOrderDraft } from './types.ts';
|
||||
|
||||
export type HostRecording = {
|
||||
readonly views: ViewBlockLike[][];
|
||||
readonly htmls: string[];
|
||||
readonly says: string[];
|
||||
readonly workOrders: WorkOrderDraft[];
|
||||
readonly cards: ChatCardDraft[];
|
||||
readonly toolCalls: Array<{ tool: string; input: Record<string, unknown> }>;
|
||||
readonly intents: Array<{ tool: string; input: Record<string, unknown>; label: string }>;
|
||||
readonly listeners: Array<{ eventType: string; keyFilter: Record<string, string>; deadline: string }>;
|
||||
readonly llmCalls: Array<{ model: string; prompt: string }>;
|
||||
readonly writes: Array<{ address: string; key: LedgerKey; patch: LedgerRow }>;
|
||||
};
|
||||
|
||||
export type HostOptions = {
|
||||
readonly contract: ContractSpec;
|
||||
/** mock 工具表:工具名 → 固定出参 */
|
||||
readonly tools?: Readonly<Record<string, (input: Record<string, unknown>) => Record<string, unknown>>>;
|
||||
/** mock 模型:模型名 → 固定出参 */
|
||||
readonly models?: Readonly<Record<string, (prompt: string) => { text: string; object?: unknown }>>;
|
||||
/** 台账初值:地址 → 行 */
|
||||
readonly ledger?: Readonly<Record<string, LedgerRow[]>>;
|
||||
readonly memory?: ReadonlyArray<{ layer: 'fact' | 'pattern' | 'verdict' | 'exception'; statement: string }>;
|
||||
};
|
||||
|
||||
export type MockHost = {
|
||||
readonly base: ActivityBase;
|
||||
readonly recording: HostRecording;
|
||||
run(definition: ActivityDefinition): Promise<ActivityOutcome>;
|
||||
};
|
||||
|
||||
export function createMockHost(options: HostOptions): MockHost {
|
||||
const recording: HostRecording = {
|
||||
views: [],
|
||||
htmls: [],
|
||||
says: [],
|
||||
workOrders: [],
|
||||
cards: [],
|
||||
toolCalls: [],
|
||||
intents: [],
|
||||
listeners: [],
|
||||
llmCalls: [],
|
||||
writes: [],
|
||||
};
|
||||
const ledger = new Map<string, LedgerRow[]>(Object.entries(options.ledger ?? {}));
|
||||
let cardSeq = 0;
|
||||
|
||||
const base: ActivityBase = {
|
||||
contract: options.contract,
|
||||
interaction: {
|
||||
async pushView(blocks) {
|
||||
recording.views.push([...blocks]);
|
||||
},
|
||||
async pushHtml(html) {
|
||||
if (/<\s*script\b/i.test(html)) throw new Error('视图模版不得含 <script> —— 容器无脚本执行权');
|
||||
recording.htmls.push(html);
|
||||
},
|
||||
async say(text) {
|
||||
recording.says.push(text);
|
||||
},
|
||||
async issueWorkOrder(order) {
|
||||
if (order.basis.length === 0) throw new Error('调用必附上下文:裸调用基座拒发');
|
||||
recording.workOrders.push(order);
|
||||
return { id: `wo-${(cardSeq += 1)}` };
|
||||
},
|
||||
async issueCard(card) {
|
||||
recording.cards.push(card);
|
||||
return { id: `card-${(cardSeq += 1)}` };
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
async window() {
|
||||
return [];
|
||||
},
|
||||
async recall(opts) {
|
||||
const all = options.memory ?? [];
|
||||
const layers = opts?.layers;
|
||||
return all.filter((m) => layers === undefined || layers.includes(m.layer)).slice(0, opts?.limit ?? 50);
|
||||
},
|
||||
},
|
||||
ledger: {
|
||||
async read(address, where) {
|
||||
const rows = ledger.get(address) ?? [];
|
||||
if (where === undefined) return rows;
|
||||
return rows.filter((row) => Object.entries(where).every(([k, v]) => row[k] === v));
|
||||
},
|
||||
async write(address, key, patch) {
|
||||
recording.writes.push({ address, key, patch });
|
||||
const rows = ledger.get(address) ?? [];
|
||||
const index = rows.findIndex((row) => Object.entries(key).every(([k, v]) => row[k] === v));
|
||||
if (index < 0) rows.push({ ...key, ...patch });
|
||||
else rows[index] = { ...rows[index], ...patch };
|
||||
ledger.set(address, rows);
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
async invoke(tool, input) {
|
||||
recording.toolCalls.push({ tool, input: { ...input } });
|
||||
const impl = options.tools?.[tool];
|
||||
if (impl === undefined) return { ok: false, error: { code: 'not-mocked', message: `工具未 mock:${tool}` } };
|
||||
return { ok: true, output: impl({ ...input }) };
|
||||
},
|
||||
async intend(tool, input, label) {
|
||||
recording.intents.push({ tool, input: { ...input }, label });
|
||||
},
|
||||
async listen(input) {
|
||||
if (input.deadline.trim() === '') throw new Error('监听时限必配 —— 无死等');
|
||||
recording.listeners.push({ ...input, keyFilter: { ...input.keyFilter } });
|
||||
return { id: `listener-${(cardSeq += 1)}` };
|
||||
},
|
||||
},
|
||||
llm: {
|
||||
async generate(input) {
|
||||
recording.llmCalls.push({ model: input.model, prompt: input.prompt });
|
||||
const impl = options.models?.[input.model];
|
||||
if (impl === undefined) throw new Error(`模型未 mock:${input.model}`);
|
||||
return impl(input.prompt);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return { base, recording, run: (definition) => definition.run(base) };
|
||||
}
|
||||
Reference in New Issue
Block a user