M3(完成): interaction 与 activity-registry
interaction(L3): - 控制权按态转交:活动在跑归活动,流转/审批/终态归系统;输入侧永远归系统(单入口) - 视图区净化门:含 <script> / on* / 外部资源的模版一律拒推 —— 容器无脚本执行权 - 对话流单入口:自由输入同写任务记忆并抛人事件;SSE 单连接 + 按目标失效广播 - 前端零过程状态:对话流与视图都是服务端投影 activity-registry(L4): - 安装两审:静态审(签名/SDK/工具契约齐备/模型名已绑定/schema/悬空监听/模版无脚本/ 零环境能力/动态执行/验收等级不得谎报)+ 回放审(旧成不坏是硬门,旧败能成只入报告) - 资产与包分离版本化:包只携资产初始版,不覆盖已有更高资产版;装载 = 包版本 + 生效资产版 - 进化管线状态机:候选 → 回放 → 人放行 → 生效;回退是显式治理动作 245 个测试通过,门禁五道全绿 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
d96c6e6dff
commit
2850e6d4ef
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* interaction 用例 —— 编排 domain,依赖 port.ts(编码规范 §5.3)。
|
||||
*/
|
||||
import type { ChatMessage, StaffId, TaskId, ViewBlock } from '@contracts';
|
||||
import { cardIdSchema } from '@contracts';
|
||||
import type { InteractionService, PushEnvelope, Subscription } from '../../contract.ts';
|
||||
import { TemplateRejectedError } from '../../contract.ts';
|
||||
import type { Audience, Clock, HumanEventPublisher, IdGenerator, InteractionStore, MemoryRecorder } from '../../port.ts';
|
||||
import type { InteractionPhase } from '../domain/control-handover.ts';
|
||||
import { checkTemplate, outputControl } from '../domain/control-handover.ts';
|
||||
|
||||
export type InteractionServiceDeps = {
|
||||
readonly store: InteractionStore;
|
||||
readonly events: HumanEventPublisher;
|
||||
readonly memory: MemoryRecorder;
|
||||
readonly audience: Audience;
|
||||
readonly clock: Clock;
|
||||
readonly ids: IdGenerator;
|
||||
};
|
||||
|
||||
type Subscriber = { staffId: StaffId; onPush: (envelope: PushEnvelope) => void };
|
||||
|
||||
export function createInteractionService(deps: InteractionServiceDeps): InteractionService {
|
||||
// SSE 订阅者表是**连接级**状态,不是过程状态:进程内持有,刷新即从投影重建(§9.2)
|
||||
const subscribers = new Set<Subscriber>();
|
||||
|
||||
function broadcast(envelope: PushEnvelope): void {
|
||||
for (const subscriber of subscribers) {
|
||||
try {
|
||||
subscriber.onPush(envelope);
|
||||
} catch {
|
||||
subscribers.delete(subscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async timeline(taskId) {
|
||||
return deps.store.listMessages(taskId);
|
||||
},
|
||||
|
||||
/** 负责人的自由输入 —— 人事件的第二形态;写入任务记忆,不驱动 flow 步 */
|
||||
async say(taskId: TaskId, by: StaffId, text: string): Promise<ChatMessage> {
|
||||
const message: ChatMessage = {
|
||||
k: 'human',
|
||||
id: cardIdSchema.parse(deps.ids.next()),
|
||||
at: deps.clock.now().toISOString(),
|
||||
text,
|
||||
by,
|
||||
};
|
||||
await deps.store.appendMessage(taskId, message);
|
||||
await deps.memory.append({ taskId, kind: 'human-input', title: text, detail: { by } });
|
||||
await deps.events.publishHumanEvent({ taskId, cardId: message.id, payload: { kind: 'free-input', text } });
|
||||
broadcast({ kind: 'chat', taskId, message });
|
||||
return message;
|
||||
},
|
||||
|
||||
async pushView(taskId: TaskId, blocks: readonly ViewBlock[]) {
|
||||
await deps.store.putView(taskId, { blocks });
|
||||
broadcast({ kind: 'view', taskId, blocks });
|
||||
},
|
||||
|
||||
/** 视图模版过净化门:含脚本 / 事件处理器 / 外部资源一律拒推 */
|
||||
async pushViewHtml(taskId, html) {
|
||||
const violations = checkTemplate(html);
|
||||
if (violations.length > 0) throw new TemplateRejectedError(violations);
|
||||
await deps.store.putView(taskId, { html });
|
||||
broadcast({ kind: 'view-html', taskId, html });
|
||||
},
|
||||
|
||||
async setPhase(taskId: TaskId, phase: InteractionPhase) {
|
||||
await deps.store.setPhase(taskId, phase);
|
||||
const output = outputControl(phase);
|
||||
broadcast({ kind: 'control', taskId, phase, output });
|
||||
return output;
|
||||
},
|
||||
|
||||
async phaseOf(taskId) {
|
||||
return (await deps.store.phaseOf(taskId)) ?? 'activating';
|
||||
},
|
||||
|
||||
subscribe(staffId, onPush) {
|
||||
const subscriber: Subscriber = { staffId, onPush };
|
||||
subscribers.add(subscriber);
|
||||
return { close: () => void subscribers.delete(subscriber) } satisfies Subscription;
|
||||
},
|
||||
|
||||
invalidate(target, id) {
|
||||
broadcast({ kind: 'invalidate', target, id });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 供 app 的 SSE 路由复用:按可见性过滤(避免把别人的任务推给不相干的人) */
|
||||
export async function visibleTo(audience: Audience, staffId: StaffId, taskId: TaskId): Promise<boolean> {
|
||||
return audience.canSee(staffId, taskId);
|
||||
}
|
||||
Reference in New Issue
Block a user