feat(teacher): 连接 Yuxi 老师并提供项目与会话只读工具
This commit is contained in:
@@ -24,10 +24,11 @@ export async function handleCodingTeacherRoutes(
|
||||
/^\/api\/coding\/teacher-preview\/topics(?:\/([^/]+))?(?:\/(messages|events|save|requests\/([^/]+)\/cancel))?$/
|
||||
);
|
||||
const config = url.pathname === '/api/coding/teacher/config';
|
||||
const catalog = url.pathname === '/api/coding/teacher/teachers';
|
||||
const draft = url.pathname === '/api/coding/teacher-preview';
|
||||
const pending = url.pathname === '/api/coding/teacher-preview/pending-link';
|
||||
if (!source && !preview && !config && !draft && !pending) return false;
|
||||
if ((config || draft || pending) && req.method !== 'GET') {
|
||||
if (!source && !preview && !config && !catalog && !draft && !pending) return false;
|
||||
if ((config || catalog || draft || pending) && req.method !== 'GET') {
|
||||
sendJson(res, 405, { error: '不支持此操作。' });
|
||||
return true;
|
||||
}
|
||||
@@ -41,6 +42,10 @@ export async function handleCodingTeacherRoutes(
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
if (catalog) {
|
||||
sendJson(res, 200, await service.catalog());
|
||||
return true;
|
||||
}
|
||||
if (config && req.method === 'GET') {
|
||||
sendJson(res, 200, await service.definition());
|
||||
return true;
|
||||
@@ -63,8 +68,8 @@ export async function handleCodingTeacherRoutes(
|
||||
return true;
|
||||
}
|
||||
if (!id && req.method === 'POST') {
|
||||
const body = await parseJsonBody<{ draftRevision?: number; sampleContext?: string }>(req);
|
||||
sendJson(res, 201, await service.create(scope, body.draftRevision, body.sampleContext));
|
||||
const body = await parseJsonBody<{ draftRevision?: number; sampleContext?: string; teacherVersion?: number }>(req);
|
||||
sendJson(res, 201, await service.create(scope, body.draftRevision, body.sampleContext, body.teacherVersion));
|
||||
return true;
|
||||
}
|
||||
if (id && !action && req.method === 'GET') {
|
||||
|
||||
363
electron/coding-teacher/cloud-runner.ts
Normal file
363
electron/coding-teacher/cloud-runner.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import type { PublicUsage } from '../../shared/coding-conversation-contracts';
|
||||
import type { TeacherTopic } from '../../shared/coding-teacher';
|
||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||
import {
|
||||
assertTeacherAccount,
|
||||
teacherCloudRequest,
|
||||
TeacherError,
|
||||
type TeacherAccount,
|
||||
} from './config-client';
|
||||
import type { TeacherModelMessage } from './context';
|
||||
import { createTeacherReadTools, type TeacherReadAccess } from './read-tools';
|
||||
|
||||
interface TeacherSession {
|
||||
access_token: string;
|
||||
expires_at: number;
|
||||
api_base_url: string;
|
||||
scope: string;
|
||||
}
|
||||
type Json = Record<string, unknown>;
|
||||
function object(value: unknown): Json {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
throw new TeacherError(502, 'teacher_protocol_invalid', '老师服务返回的数据无效。');
|
||||
return value as Json;
|
||||
}
|
||||
function identifier(value: unknown): string {
|
||||
if (typeof value !== 'string' || !value)
|
||||
throw new TeacherError(502, 'teacher_protocol_invalid', '老师服务未返回有效的运行标识。');
|
||||
return value;
|
||||
}
|
||||
export interface TeacherCloudTransport {
|
||||
json(path: string, body?: unknown, signal?: AbortSignal): Promise<Json>;
|
||||
events(
|
||||
path: string,
|
||||
signal: AbortSignal,
|
||||
accept: (event: string, data: Json, id: string) => void
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export function teacherCloudTransport(account: TeacherAccount): TeacherCloudTransport {
|
||||
let session: TeacherSession | undefined;
|
||||
const fetchCloud = async (path: string, body?: unknown, signal?: AbortSignal) => {
|
||||
// 退出账号后只允许用已经持有的短凭据停止原问题,不能再读状态或发送内容。
|
||||
const stopping = /^\/questions\/[^/]+\/cancel$/.test(path) && body !== undefined;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
if (!stopping || !session) assertTeacherAccount(account);
|
||||
signal?.throwIfAborted();
|
||||
if (!session || (!stopping && session.expires_at * 1000 < Date.now() + 10000)) {
|
||||
session = await teacherCloudRequest<TeacherSession>(
|
||||
account,
|
||||
'/api/coding-teacher/session',
|
||||
'POST'
|
||||
);
|
||||
if (
|
||||
session.scope !== 'makelore-teachers' ||
|
||||
typeof session.access_token !== 'string' ||
|
||||
!session.access_token ||
|
||||
!Number.isFinite(session.expires_at) ||
|
||||
session.expires_at * 1000 <= Date.now() ||
|
||||
typeof session.api_base_url !== 'string' ||
|
||||
!/^https?:\/\//.test(session.api_base_url)
|
||||
) {
|
||||
throw new TeacherError(502, 'teacher_session_invalid', '老师接入凭据无效,请重新连接。');
|
||||
}
|
||||
}
|
||||
if (!stopping) assertTeacherAccount(account);
|
||||
try {
|
||||
const response = await proxyAwareFetch(
|
||||
session.api_base_url.replace(/\/+$/, '') + '/api/makelore/teachers' + path,
|
||||
{
|
||||
method: body === undefined ? 'GET' : 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + session.access_token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(45000)]),
|
||||
}
|
||||
);
|
||||
if (!stopping) assertTeacherAccount(account);
|
||||
if (response.status === 401 && attempt < 2) {
|
||||
await response.body?.cancel();
|
||||
session = undefined;
|
||||
continue;
|
||||
}
|
||||
if (response.status >= 500 && attempt < 2) {
|
||||
await response.body?.cancel();
|
||||
await delay(500, undefined, { signal });
|
||||
continue;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const data = object(await response.json());
|
||||
const detail = data.detail;
|
||||
const message =
|
||||
typeof detail === 'string'
|
||||
? detail
|
||||
: detail && typeof detail === 'object'
|
||||
? object(detail).message
|
||||
: undefined;
|
||||
throw new TeacherError(
|
||||
response.status,
|
||||
'teacher_cloud_failed',
|
||||
typeof message === 'string' ? message : '老师服务暂不可用,请稍后重试。'
|
||||
);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
signal?.throwIfAborted();
|
||||
if (error instanceof TeacherError || attempt === 2) throw error;
|
||||
await delay(500, undefined, { signal });
|
||||
}
|
||||
}
|
||||
throw new TeacherError(502, 'teacher_connection_failed', '连接老师失败。');
|
||||
};
|
||||
return {
|
||||
async json(path, body, signal) {
|
||||
const response = await fetchCloud(path, body, signal);
|
||||
const data = object(await response.json());
|
||||
if (!path.endsWith('/cancel')) assertTeacherAccount(account);
|
||||
return data;
|
||||
},
|
||||
async events(path, signal, accept) {
|
||||
const response = await fetchCloud(path, undefined, signal);
|
||||
if (!response.body)
|
||||
throw new TeacherError(502, 'teacher_stream_missing', '老师回复连接不可用。');
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
try {
|
||||
while (true) {
|
||||
const next = await reader.read();
|
||||
if (next.done) break;
|
||||
assertTeacherAccount(account);
|
||||
signal.throwIfAborted();
|
||||
buffer += decoder.decode(next.value, { stream: true }).replaceAll('\r', '');
|
||||
let end: number;
|
||||
while ((end = buffer.indexOf('\n\n')) >= 0) {
|
||||
const block = buffer.slice(0, end);
|
||||
buffer = buffer.slice(end + 2);
|
||||
let event = 'message',
|
||||
id = '';
|
||||
const data: string[] = [];
|
||||
for (const line of block.split('\n')) {
|
||||
if (line.startsWith('event:')) event = line.slice(6).trim();
|
||||
else if (line.startsWith('id:')) id = line.slice(3).trim();
|
||||
else if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
|
||||
}
|
||||
if (data.length) accept(event, object(JSON.parse(data.join('\n'))), id);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await reader.cancel().catch(() => undefined);
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareCloudTeacher(
|
||||
account: TeacherAccount,
|
||||
topic: TeacherTopic,
|
||||
requestId: string,
|
||||
access: TeacherReadAccess,
|
||||
onProgress: (text: string) => void,
|
||||
saveRequestId: (id: string) => Promise<void>,
|
||||
transport: TeacherCloudTransport = teacherCloudTransport(account)
|
||||
) {
|
||||
const tools = createTeacherReadTools(access);
|
||||
return {
|
||||
inputLimit: topic.definition.limits.max_input_tokens,
|
||||
async run(
|
||||
messages: TeacherModelMessage[],
|
||||
signal: AbortSignal,
|
||||
onText: (delta: string) => void
|
||||
): Promise<PublicUsage | undefined> {
|
||||
const localContext = {
|
||||
id: requestId,
|
||||
scope: { project_id: topic.projectId, source_session_id: topic.sourceConversationId },
|
||||
tools: tools.definitions.map((item) => item.function.name),
|
||||
};
|
||||
const deadline = Date.now() + 60 * 60 * 1000;
|
||||
const bounded = AbortSignal.any([signal, AbortSignal.timeout(60 * 60 * 1000)]);
|
||||
let questionId: string | undefined;
|
||||
let completed = false;
|
||||
try {
|
||||
// 重启后本地快照已经丢失,先停止旧请求,再以新的问题建立读取作用域。
|
||||
const previous = topic.requests.filter((item) => item.id !== requestId).at(-1);
|
||||
if (previous && previous.status !== 'completed') {
|
||||
try {
|
||||
await transport.json(
|
||||
'/questions/' + encodeURIComponent(previous.id) + '/cancel',
|
||||
{},
|
||||
bounded
|
||||
);
|
||||
} catch (error) {
|
||||
if (!(error instanceof TeacherError && error.status === 404)) throw error;
|
||||
}
|
||||
}
|
||||
let queued = await transport.json(
|
||||
'/questions',
|
||||
{
|
||||
teacher_version: topic.version,
|
||||
thread_id: topic.id,
|
||||
request_id: requestId,
|
||||
query: messages
|
||||
.filter((item) => item.role === 'user')
|
||||
.map((item) => item.content)
|
||||
.join('\n\n'),
|
||||
local_context: localContext,
|
||||
},
|
||||
bounded
|
||||
);
|
||||
questionId = identifier(queued.request_id);
|
||||
await saveRequestId(questionId);
|
||||
while (!queued.run_id) {
|
||||
if (!['queued', 'pending'].includes(String(queued.status)))
|
||||
throw new TeacherError(
|
||||
409,
|
||||
'teacher_request_stopped',
|
||||
'老师提问未能启动,请重新提问。'
|
||||
);
|
||||
onProgress('正在等待老师…');
|
||||
await delay(800, undefined, { signal: bounded });
|
||||
queued = await transport.json(
|
||||
'/requests/' + encodeURIComponent(questionId),
|
||||
undefined,
|
||||
bounded
|
||||
);
|
||||
}
|
||||
let runId = identifier(queued.run_id),
|
||||
cursor = '0-0',
|
||||
rounds = 0;
|
||||
let runText = '',
|
||||
messageId = '';
|
||||
while (Date.now() < deadline) {
|
||||
bounded.throwIfAborted();
|
||||
access.assertCurrent();
|
||||
const view = await transport.json(
|
||||
'/runs/' + encodeURIComponent(runId),
|
||||
undefined,
|
||||
bounded
|
||||
);
|
||||
if (view.continued_run_id) {
|
||||
runId = identifier(view.continued_run_id);
|
||||
cursor = '0-0';
|
||||
runText = '';
|
||||
messageId = '';
|
||||
continue;
|
||||
}
|
||||
if (view.status === 'interrupted') {
|
||||
const pending = object(view.interrupt);
|
||||
if (pending.source !== 'client_read_tools')
|
||||
throw new TeacherError(
|
||||
409,
|
||||
'teacher_interaction_unsupported',
|
||||
'老师请求了当前面板不支持的交互,请联系运营调整该智能体。'
|
||||
);
|
||||
if (
|
||||
pending.context_id !== requestId ||
|
||||
!Array.isArray(pending.calls) ||
|
||||
!pending.calls.length ||
|
||||
pending.calls.length > 32 ||
|
||||
++rounds > 6
|
||||
)
|
||||
throw new TeacherError(
|
||||
409,
|
||||
'teacher_context_expired',
|
||||
'老师读取请求已失效,请重新提问。'
|
||||
);
|
||||
onProgress('正在读取项目与会话…');
|
||||
const results = [];
|
||||
for (const raw of pending.calls) {
|
||||
const call = object(raw);
|
||||
if (typeof call.tool_call_id !== 'string' || typeof call.name !== 'string')
|
||||
throw new TeacherError(502, 'teacher_protocol_invalid', '老师读取请求无效。');
|
||||
results.push({
|
||||
tool_call_id: call.tool_call_id,
|
||||
...(await tools.executeResult(call.name, JSON.stringify(call.arguments), bounded)),
|
||||
});
|
||||
}
|
||||
// POST 重试使用完全相同的结果,文件变化也不会导致重复续接或不同输入。
|
||||
const resumed = await transport.json(
|
||||
'/runs/' + encodeURIComponent(runId) + '/tool-results',
|
||||
{ context_id: requestId, results },
|
||||
bounded
|
||||
);
|
||||
runId = identifier(resumed.run_id);
|
||||
cursor = '0-0';
|
||||
runText = '';
|
||||
messageId = '';
|
||||
onProgress('老师正在继续思考…');
|
||||
continue;
|
||||
}
|
||||
if (view.status === 'completed') {
|
||||
const output = typeof view.output === 'string' ? view.output : '';
|
||||
if (output.startsWith(runText)) onText(output.slice(runText.length));
|
||||
else if (output) onText('\n\n' + output);
|
||||
completed = true;
|
||||
onProgress('');
|
||||
// Yuxi 的账本记录每个模型调用;不把线程累计 token 当作本问题费用。
|
||||
return undefined;
|
||||
}
|
||||
if (['failed', 'cancelled'].includes(String(view.status))) {
|
||||
const detail = view.error ? object(view.error).message : undefined;
|
||||
throw new TeacherError(
|
||||
409,
|
||||
'teacher_run_failed',
|
||||
typeof detail === 'string' ? detail : '老师回复已停止。'
|
||||
);
|
||||
}
|
||||
onProgress('老师正在思考…');
|
||||
try {
|
||||
await transport.events(
|
||||
'/runs/' +
|
||||
encodeURIComponent(runId) +
|
||||
'/events?after_seq=' +
|
||||
encodeURIComponent(cursor),
|
||||
bounded,
|
||||
(_event, envelope, id) => {
|
||||
if (id) cursor = id;
|
||||
const payload = envelope.payload ? object(envelope.payload) : {};
|
||||
for (const item of Array.isArray(payload.items)
|
||||
? payload.items
|
||||
: payload.chunk
|
||||
? [payload.chunk]
|
||||
: []) {
|
||||
const chunk = object(item);
|
||||
const event = chunk.stream_event ? object(chunk.stream_event) : {};
|
||||
if (event.type === 'message_delta' && typeof event.content === 'string') {
|
||||
if (typeof event.message_id === 'string' && event.message_id !== messageId) {
|
||||
if (messageId && runText) onText('\n\n');
|
||||
messageId = event.message_id;
|
||||
runText = '';
|
||||
}
|
||||
runText += event.content;
|
||||
onText(event.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
bounded.throwIfAborted();
|
||||
access.assertCurrent();
|
||||
if (error instanceof TeacherError && error.status < 500) throw error;
|
||||
onProgress('连接中断,正在恢复老师回复…');
|
||||
}
|
||||
await delay(400, undefined, { signal: bounded });
|
||||
}
|
||||
throw new TeacherError(408, 'teacher_question_expired', '本次老师提问已超时,请重新提问。');
|
||||
} finally {
|
||||
if (!completed) {
|
||||
await transport
|
||||
.json(
|
||||
'/questions/' + encodeURIComponent(requestId) + '/cancel',
|
||||
{},
|
||||
AbortSignal.timeout(10000)
|
||||
)
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
isCurrentWorksSquareAccountBinding,
|
||||
type WorksSquareAccountBinding,
|
||||
} from '../services/works-square-session';
|
||||
import type { TeacherAvailability, TeacherDefinition } from '../../shared/coding-teacher';
|
||||
import type { TeacherAvailability, TeacherDefinition, TeacherCatalog } from '../../shared/coding-teacher';
|
||||
|
||||
export class TeacherError extends Error {
|
||||
constructor(
|
||||
@@ -27,7 +27,8 @@ export function assertTeacherAccount(account: TeacherAccount) {
|
||||
}
|
||||
export async function teacherCloudRequest<T>(
|
||||
account: TeacherAccount,
|
||||
pathname: string
|
||||
pathname: string,
|
||||
method: 'GET' | 'POST' = 'GET'
|
||||
): Promise<T> {
|
||||
assertTeacherAccount(account);
|
||||
const token = await getValidWorksSquareAccessToken();
|
||||
@@ -36,6 +37,7 @@ export async function teacherCloudRequest<T>(
|
||||
const response = await proxyAwareFetch(
|
||||
WORKS_SQUARE_CONFIG.apiBaseUrl.replace(/\/+$/, '') + pathname,
|
||||
{
|
||||
method,
|
||||
headers: { Authorization: 'Bearer ' + token },
|
||||
signal: AbortSignal.timeout(30000),
|
||||
}
|
||||
@@ -69,6 +71,8 @@ export async function currentTeacherAccount(): Promise<TeacherAccount> {
|
||||
}
|
||||
export const teacherAvailability = (account: TeacherAccount) =>
|
||||
teacherCloudRequest<TeacherAvailability>(account, '/api/coding-teacher/config');
|
||||
export const teacherCatalog = (account: TeacherAccount) =>
|
||||
teacherCloudRequest<TeacherCatalog>(account, '/api/coding-teacher/teachers');
|
||||
export const teacherVersion = (account: TeacherAccount, version: number) =>
|
||||
teacherCloudRequest<{ version: number; payload: TeacherDefinition }>(
|
||||
account,
|
||||
|
||||
@@ -56,9 +56,14 @@ export function createTeacherReadTools(access: TeacherReadAccess) {
|
||||
return {
|
||||
definitions: teacherReadToolDefinitions,
|
||||
async execute(name: string, rawArguments: string, signal: AbortSignal, maxBytes = 2400): Promise<string> {
|
||||
return (await this.executeResult(name, rawArguments, signal, maxBytes)).content;
|
||||
},
|
||||
async executeResult(name: string, rawArguments: string, signal: AbortSignal, maxBytes = 2400): Promise<{ status: 'success' | 'error'; content: string; truncated: boolean }> {
|
||||
signal.throwIfAborted();
|
||||
access.assertCurrent();
|
||||
let result: string;
|
||||
let status: 'success' | 'error' = 'success';
|
||||
let truncated = false;
|
||||
try {
|
||||
const args = JSON.parse(rawArguments) as Record<string, unknown>;
|
||||
if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('Expected an object.');
|
||||
@@ -71,6 +76,7 @@ export function createTeacherReadTools(access: TeacherReadAccess) {
|
||||
}
|
||||
case 'read_project_file': {
|
||||
const file = await files.content(access.projectPath, projectPath(args.path));
|
||||
truncated = file.truncated;
|
||||
result = file.path + '\n' + lines(file.content, args)
|
||||
+ (file.truncated ? '\n[File exceeds the 256 KiB text preview limit; only its beginning is available.]' : '');
|
||||
break;
|
||||
@@ -90,13 +96,15 @@ export function createTeacherReadTools(access: TeacherReadAccess) {
|
||||
throw new Error('Only list_project_files, read_project_file and read_conversation are available.');
|
||||
}
|
||||
} catch (error) {
|
||||
status = 'error';
|
||||
// Keep local OS paths and unrelated application data out of model errors.
|
||||
result = 'Read failed. Check the relative path, message id and line range. '
|
||||
+ (error instanceof SyntaxError ? 'Tool arguments must be valid JSON.' : 'Only current-project text files and current-conversation messages are available.');
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
access.assertCurrent();
|
||||
return excerptTeacherText(result, maxBytes);
|
||||
const content = excerptTeacherText(result, maxBytes);
|
||||
return { status, content, truncated: truncated || content !== result };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user