Add teacher microphone and per-question model selection

This commit is contained in:
鲨鱼辣椒
2026-09-29 19:43:45 +08:00
parent cd1ba41e21
commit 78cfde6734
20 changed files with 517 additions and 37 deletions

View File

@@ -251,6 +251,7 @@ export function prepareCloudTeacher(
'/questions',
{
teacher_version: currentRequest?.teacherVersion ?? topic.version,
...(currentRequest?.modelId ? { model_id: currentRequest.modelId } : {}),
thread_id: currentRequest?.runtimeThreadId ?? topic.id,
request_id: requestId,
query,

View File

@@ -0,0 +1,38 @@
import type { TeacherModelCatalog } from '../../shared/coding-teacher';
import { normalizeManagedModelCatalog } from '../../shared/managed-model-capabilities';
import { teacherCloudRequest, assertTeacherAccount, TeacherError, type TeacherAccount } from './config-client';
import { teacherCloudTransport } from './cloud-runner';
export function validateTeacherModelId(value: unknown): string | undefined {
if (value === undefined) return undefined;
if (typeof value !== 'string' || !value.trim() || value.length > 200 || value !== value.trim())
throw new TeacherError(422, 'teacher_model_invalid', '请选择有效的模型。');
return value;
}
/** Main strips credentials; each selected model is revalidated when the question is accepted. */
export async function teacherModelCatalog(account: TeacherAccount, runtime: 'local' | 'yuxi'): Promise<TeacherModelCatalog> {
if (runtime === 'yuxi') {
let result;
try { result = await teacherCloudTransport(account).json('/models'); }
catch (error) {
if (error instanceof TeacherError && error.status === 404) return {
supported: false, models: [], message: '老师服务更新后即可切换模型,目前使用老师配置。',
};
throw error;
}
assertTeacherAccount(account);
if (result.model_selection === false) return { supported: false, models: [], message: '老师服务更新后即可切换模型,目前使用老师配置。' };
if (result.model_selection !== true || !Array.isArray(result.models)
|| result.models.some(item => !item || typeof item !== 'object' || typeof item.id !== 'string' || !item.id || item.id.length > 200))
throw new TeacherError(502, 'teacher_models_invalid', '模型列表暂不可用,请重试。');
const local = await teacherModelCatalog(account, 'local');
const textModels = new Set(local.models.map(item => item.id));
return { supported: true, models: result.models.filter(item => textModels.has(item.id)).map(item => ({ id: item.id as string })) };
}
const config = await teacherCloudRequest<{ models: string[]; model_capabilities_v2: unknown }>(account, '/api/auth/me/model-config');
const catalog = normalizeManagedModelCatalog(config.model_capabilities_v2, config.models);
assertTeacherAccount(account);
return { supported: true, models: config.models.filter(id => catalog?.models[id]?.inputModalities?.includes('text')
&& catalog.models[id]?.outputModalities?.includes('text')).map(id => ({ id })) };
}

View File

@@ -32,6 +32,7 @@ import { TeacherConversationStore } from './conversation-store';
import { readJsonFile } from '../coding-projects/atomic-json';
import { compileTeacherContext } from './context';
import { prepareTeacherModel } from './model-runner';
import { teacherModelCatalog, validateTeacherModelId } from './model-catalog';
import { prepareCloudTeacher } from './cloud-runner';
import { readTeacherSource } from './source-reader';
import { applyTeacherReply, teacherReplyInstructions } from './reply';
@@ -59,6 +60,7 @@ export interface TeacherServiceOptions {
prepareModel?: typeof prepareTeacherModel;
prepareCloud?: typeof prepareCloudTeacher;
catalog?: typeof teacherCatalog;
models?: typeof teacherModelCatalog;
readSource?(scope: TeacherScope): Promise<TeacherSourceContext>;
}
export class CodingTeacherService {
@@ -100,6 +102,9 @@ export class CodingTeacherService {
: null;
return { ...status, definition: published?.payload ?? null };
}
async models(runtime: 'local' | 'yuxi') {
return (this.options.models ?? teacherModelCatalog)(await this.account(), runtime);
}
async catalog() {
return (this.options.catalog ?? teacherCatalog)(await this.account());
}
@@ -415,6 +420,7 @@ export class CodingTeacherService {
checkIn?: { source: TeacherSourceContext; fingerprint: string }
): Promise<TeacherTopic> {
teacherTopicId(input.requestId);
const modelId = validateTeacherModelId(input.modelId);
const intent = input.intent === undefined ? 'question' : input.intent;
const structuredReply = intent !== 'check-in';
if (!['question', 'suggestions', 'guided-help', 'check-in'].includes(intent) || (intent === 'check-in' && !checkIn))
@@ -452,6 +458,7 @@ export class CodingTeacherService {
if (existing) {
if (
existing.text !== input.text ||
existing.modelId !== modelId ||
(scope.agentId && existing.projectId !== scope.projectId) ||
(existing.intent ?? 'question') !== intent ||
JSON.stringify(existing.references) !== JSON.stringify(refs) ||
@@ -479,6 +486,7 @@ export class CodingTeacherService {
const previous = topic.requests.at(-1);
const reuse = previous && !previous.origin && previous.projectId === scope.projectId
&& previous.sourceConversationId === input.sourceConversationId
&& previous.modelId === modelId
&& previous.presentation === 'reply-v1'
&& previous.teacherVersion === selected.version && previous.runtimeThreadId;
newConversationSegment = !reuse;
@@ -545,6 +553,15 @@ export class CodingTeacherService {
return structuredClone(ref);
});
const isCloud = topic.definition.runtime === 'yuxi';
if (modelId) {
const models = await (this.options.models ?? teacherModelCatalog)(account, isCloud ? 'yuxi' : 'local');
if (!models.supported || !models.models.some(item => item.id === modelId))
throw new TeacherError(422, 'teacher_model_unavailable', models.message ?? '这个模型暂不可用,请重新选择。');
this.assertAccount(account);
}
const modelDefinition = modelId
? { ...topic.definition, model: { model_id: modelId, reasoning_choice: { mode: 'default' as const } } }
: topic.definition;
if (!isCloud && !topic.definition.system_prompt.trim())
throw new TeacherError(422, 'teacher_definition_invalid', '请先配置智能体的系统提示词。');
const access = scope.projectId === 'preview' ? undefined : {
@@ -573,7 +590,7 @@ export class CodingTeacherService {
topic.revision++;
this.events.emit(key, structuredClone(topic));
})
: await (this.options.prepareModel ?? prepareTeacherModel)(account, topic.definition,
: await (this.options.prepareModel ?? prepareTeacherModel)(account, modelDefinition,
access,
{ finalOnly: structuredReply });
if (checkIn) this.assertAccount(account);
@@ -614,6 +631,7 @@ export class CodingTeacherService {
text: input.text,
references,
createdAt: new Date().toISOString(),
...(modelId ? { modelId } : {}),
sourceCursor: source.cursor,
sourceCapturedAt: source.capturedAt,
includedSourceMessageIds: compiled.includedSourceMessageIds,