Merge teacher composer and compact prompt UI into main
This commit is contained in:
@@ -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,
|
||||
|
||||
38
electron/coding-teacher/model-catalog.ts
Normal file
38
electron/coding-teacher/model-catalog.ts
Normal 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 })) };
|
||||
}
|
||||
@@ -33,6 +33,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';
|
||||
@@ -60,6 +61,7 @@ export interface TeacherServiceOptions {
|
||||
prepareModel?: typeof prepareTeacherModel;
|
||||
prepareCloud?: typeof prepareCloudTeacher;
|
||||
catalog?: typeof teacherCatalog;
|
||||
models?: typeof teacherModelCatalog;
|
||||
readSource?(scope: TeacherScope): Promise<TeacherSourceContext>;
|
||||
}
|
||||
export class CodingTeacherService {
|
||||
@@ -101,6 +103,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());
|
||||
}
|
||||
@@ -421,6 +426,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))
|
||||
@@ -458,6 +464,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) ||
|
||||
@@ -485,6 +492,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;
|
||||
@@ -551,6 +559,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 : {
|
||||
@@ -579,7 +596,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);
|
||||
@@ -620,6 +637,7 @@ export class CodingTeacherService {
|
||||
text: input.text,
|
||||
references,
|
||||
createdAt: new Date().toISOString(),
|
||||
...(modelId ? { modelId } : {}),
|
||||
sourceCursor: source.cursor,
|
||||
sourceCapturedAt: source.capturedAt,
|
||||
includedSourceMessageIds: compiled.includedSourceMessageIds,
|
||||
@@ -677,7 +695,8 @@ export class CodingTeacherService {
|
||||
current.response = '';
|
||||
delete current.suggestedQuestions;
|
||||
}
|
||||
topic.updatedAt = new Date().toISOString();
|
||||
current.finishedAt = new Date().toISOString();
|
||||
topic.updatedAt = current.finishedAt;
|
||||
topic.revision++;
|
||||
try {
|
||||
await store.save(topic, current.id);
|
||||
|
||||
Reference in New Issue
Block a user