feat(teacher): 连接 Yuxi 老师并提供项目与会话只读工具

This commit is contained in:
2026-09-23 09:56:11 +08:00
parent 1f2ad3fb3a
commit e35a13079e
14 changed files with 886 additions and 30 deletions

View File

@@ -17,12 +17,14 @@ import {
teacherAvailability,
teacherVersion,
teacherPreview,
teacherCatalog,
TeacherError,
type TeacherAccount,
} from './config-client';
import { TeacherTopicStore, teacherTopicId } from './store';
import { compileTeacherContext } from './context';
import { prepareTeacherModel } from './model-runner';
import { prepareCloudTeacher } from './cloud-runner';
import { readTeacherSource } from './source-reader';
import { subscribeWorksSquareSession } from '../services/works-square-session';
@@ -44,6 +46,8 @@ export interface TeacherServiceOptions {
version?: typeof teacherVersion;
preview?: typeof teacherPreview;
prepareModel?: typeof prepareTeacherModel;
prepareCloud?: typeof prepareCloudTeacher;
catalog?: typeof teacherCatalog;
readSource?(scope: TeacherScope): Promise<TeacherSourceContext>;
}
export class CodingTeacherService {
@@ -84,6 +88,9 @@ export class CodingTeacherService {
: null;
return { ...status, definition: published?.payload ?? null };
}
async catalog() {
return (this.options.catalog ?? teacherCatalog)(await this.account());
}
private async scopedStore(
account: TeacherAccount,
scope: TeacherScope
@@ -132,7 +139,8 @@ export class CodingTeacherService {
async create(
scope: TeacherScope,
draftRevision?: number,
sampleContext = ''
sampleContext = '',
teacherVersionNumber?: number,
): Promise<TeacherTopic> {
const account = await this.account();
const store = await this.scopedStore(account, scope);
@@ -148,6 +156,14 @@ export class CodingTeacherService {
definition = (await (this.options.preview ?? teacherPreview)(account, draftRevision!))
.payload;
version = 0;
} else if (teacherVersionNumber !== undefined) {
if (!Number.isSafeInteger(teacherVersionNumber) || teacherVersionNumber < 1)
throw new TeacherError(422, 'teacher_version_invalid', '老师版本无效。');
const catalog = await (this.options.catalog ?? teacherCatalog)(account);
const selected = catalog.items.find(item => item.version === teacherVersionNumber);
if (!selected) throw new TeacherError(409, 'teacher_disabled', '该老师暂未开放,请刷新后选择。');
definition = selected.definition;
version = selected.version;
} else {
const status = await (this.options.availability ?? teacherAvailability)(account);
if (!status.enabled || !status.published_version)
@@ -236,7 +252,11 @@ export class CodingTeacherService {
throw new TeacherError(409, 'teacher_topic_busy', '请等待当前回复完成,或先停止。');
if (topic.draftRevision) {
await (this.options.preview ?? teacherPreview)(account, topic.draftRevision);
} else {
} else if (topic.definition.runtime !== 'yuxi' && topic.definition.config_id) {
const catalog = await (this.options.catalog ?? teacherCatalog)(account);
if (!catalog.items.some(item => item.teacher_id === topic.definition.config_id))
throw new TeacherError(409, 'teacher_disabled', '老师已停用,历史仍可查看。');
} else if (topic.definition.runtime !== 'yuxi') {
const available = await (this.options.availability ?? teacherAvailability)(account);
if (!available.enabled)
throw new TeacherError(409, 'teacher_disabled', '老师已停用,历史仍可查看。');
@@ -272,22 +292,31 @@ export class CodingTeacherService {
);
return structuredClone(ref);
});
if (!topic.definition.system_prompt.trim())
const isCloud = topic.definition.runtime === 'yuxi';
if (!isCloud && !topic.definition.system_prompt.trim())
throw new TeacherError(422, 'teacher_definition_invalid', '请先配置老师的系统提示词。');
const model = await (this.options.prepareModel ?? prepareTeacherModel)(
account,
topic.definition,
scope.projectId === 'preview' ? undefined : {
const access = scope.projectId === 'preview' ? undefined : {
projectPath: (await this.options.projects.getProject(scope.projectId)).path,
source,
history: topic.requests,
assertCurrent: () => this.assertAccount(account),
}
);
};
const model = isCloud && access
? (this.options.prepareCloud ?? prepareCloudTeacher)(account, topic, input.requestId, access,
(progress) => {
const current = topic.requests.at(-1)!;
if (current.progress === progress) return;
current.progress = progress; topic.revision++;
this.events.emit(key, structuredClone(topic));
}, async cloudRequestId => {
topic.requests.at(-1)!.cloudRequestId = cloudRequestId;
await store.save(topic);
})
: await (this.options.prepareModel ?? prepareTeacherModel)(account, topic.definition, access);
const compiled = compileTeacherContext(
topic.definition,
source,
topic.requests,
isCloud ? [] : topic.requests,
input.text,
references,
model.inputLimit,