forked from EduCraft/curriculum-project-hub
3bca137b48
hub/ TS 新部件,平级于 spec/。本仓从零 TS 重写(非迁移旧服务),栈: Fastify+Prisma 待补;本提交落 provider-agnostic agent runtime 骨架: - provider.ts: AgentProvider seam,OpenAI-compatible messages+tools+tool_calls 形状(语言通解,非 OpenAI 承诺);@Claude 仅为触发品牌(ADR-0017) - openrouter-provider.ts: openai SDK 指向 OpenRouter 的具体适配器,仅做 Message↔SDK 翻译;agent loop 不 import SDK,换 provider 不改 loop - runner.ts: 自有 agent loop(dispatch tools/循环到 stop/budget),per-run model(ADR-0017 role-based routing,run 带其 model) - tools.ts: 窄工具面 + ADR-0003 McpReadRequest.Authorized 落代码: feishu_read_context 拒绝非本项目绑定 chat 的读取(实测错 chat 抛 UnauthorizedChatRead,对 chat 放行) - models.ts: ModelRegistry seam(role→default 映射策略 OPEN) - server.ts: 接线入口(stub Feishu read,无活凭证即可编译) tsc --noEmit rc=0;ADR-0003 不变式 smoke 通过。
65 lines
2.3 KiB
TypeScript
65 lines
2.3 KiB
TypeScript
/**
|
|
* Per-run model selection & role-based routing.
|
|
*
|
|
* ADR-0017 consequence: role-based model routing (different run kinds default
|
|
* to different models) is a **product/admin configuration** concern, not a spec
|
|
* invariant. This registry is the seam for that config; the concrete policy
|
|
* (which models are admin-enabled, which role maps to which default) is `OPEN`
|
|
* here — decided by admin settings + ADR, not hard-coded.
|
|
*/
|
|
|
|
/** Coarse run role, used to pick a default model. Open set: add as product grows. */
|
|
export type RunRole = "draft" | "review" | "triage";
|
|
|
|
/** A model the admin has enabled for use by the Hub. */
|
|
export interface ModelEntry {
|
|
readonly id: string;
|
|
/** Human label for the teacher-side switcher UX. */
|
|
readonly label: string;
|
|
/** True when the model is known to support tool use reliably. */
|
|
readonly toolCapable: boolean;
|
|
}
|
|
|
|
export interface ModelRegistry {
|
|
/** All models admin-enabled for this Hub instance. */
|
|
list(): readonly ModelEntry[];
|
|
/** Default model id for a given run role, if a routing rule exists. */
|
|
defaultFor(role: RunRole): string | undefined;
|
|
}
|
|
|
|
/**
|
|
* A simple in-memory registry. Real wiring reads from admin settings / DB; this
|
|
* is the skeleton seam. The policy (role→model map) is intentionally pluggable.
|
|
*/
|
|
export class InMemoryModelRegistry implements ModelRegistry {
|
|
private readonly models: readonly ModelEntry[];
|
|
private readonly defaults: Map<RunRole, string>;
|
|
|
|
constructor(models: readonly ModelEntry[], defaults: Partial<Record<RunRole, string>> = {}) {
|
|
this.models = models;
|
|
this.defaults = new Map(
|
|
(Object.entries(defaults) as [RunRole, string][]).filter(([, v]) => v !== undefined),
|
|
);
|
|
}
|
|
|
|
list(): readonly ModelEntry[] {
|
|
return this.models;
|
|
}
|
|
|
|
defaultFor(role: RunRole): string | undefined {
|
|
return this.defaults.get(role);
|
|
}
|
|
|
|
/** Resolve a model id, falling back to the role default, then the first enabled. */
|
|
resolve(requested: string | undefined, role: RunRole): string {
|
|
if (requested !== undefined && this.models.some((m) => m.id === requested)) {
|
|
return requested;
|
|
}
|
|
const d = this.defaultFor(role);
|
|
if (d !== undefined) return d;
|
|
const first = this.models[0];
|
|
if (first === undefined) throw new Error("no models enabled for this Hub");
|
|
return first.id;
|
|
}
|
|
}
|