build(trial): preserve deployed source for knowledge refresh

This commit is contained in:
鲨鱼辣椒 committed 2026-10-13 14:29:33 +08:00
1 parent 8e0e52f30e
commit 2d4e44f4a0
42 files changed
+2321 -283

No files matched your search

+5 -2
View File
@@ -26,14 +26,17 @@ export class CloudAgentJournal {
return this.update(accountKey, validate, value => ({ ...value, recent }));
}
put(accountKey: string, entry: CloudPendingOperation, validate: () => void) {
return this.update(accountKey, validate, value => {
async put(accountKey: string, entry: CloudPendingOperation, validate: () => void): Promise<boolean> {
let alreadyPending = false;
await this.update(accountKey, validate, value => {
const existing = value.pending.find(item => item.id === entry.id);
if (existing && JSON.stringify(existing.input) !== JSON.stringify(entry.input)) {
throw new Error('同一次待确认操作不能更改输入');
}
alreadyPending = Boolean(existing);
return { ...value, pending: existing ? value.pending : [...value.pending, entry] };
});
return alreadyPending;
}
remove(accountKey: string, id: string, validate: () => void) {
+17 -2
View File
@@ -77,11 +77,26 @@ function projectRun(value: unknown): unknown {
}
function projectConversation(value: unknown): unknown {
const source = object(value);
return { ...projectSession(source), messages: list(source.messages, item => copy(item, ['id', 'role', 'content', 'request_id', 'run_id', 'created_at'])),
return { ...projectSession(source), messages: list(source.messages, projectMessage),
run: source.run == null ? null : projectRun(source.run),
queued_requests: list(source.queued_requests, item => copy(item, ['request_id', 'thread_id', 'run_id', 'status', 'version'])),
next_offset: source.next_offset ?? null };
}
function projectMessage(value: unknown): unknown {
const source = object(value);
return { ...copy(source, ['id', 'role', 'content', 'request_id', 'run_id', 'created_at', 'message_id', 'reasoning_content']),
...(Array.isArray(source.tool_calls) ? { tool_calls: source.tool_calls.map(item => {
const tool = object(item);
return { ...copy(tool, ['id', 'name', 'display_name', 'display_description', 'args', 'status', 'error_message']),
...(tool.tool_call_result == null ? {} : { tool_call_result: copy(tool.tool_call_result, ['content']) }) };
}) } : {}),
};
}
function projectHistory(value: unknown): unknown {
const source = object(value);
return { ...copy(source, ['thread_id', 'queued_requests', 'schedule_proposals', 'next_offset']),
messages: list(source.messages, projectMessage), run: source.run == null ? null : projectRun(source.run) };
}
function projectFiles(value: unknown): unknown {
const source = object(value);
return { session_id: source.session_id, thread_id: source.thread_id ?? null,
@@ -253,7 +268,7 @@ const operations: Record<keyof CloudAgentOperations, OperationSpec> = {
saveBudget: { method: 'PUT', cloud: 'ws', path: '/agents/:slug/budget', body: ['request_limit_points', 'daily_limit_points'], output: budget },
threads: { method: 'GET', path: '/threads', query: { slug: 'slug', offset: 'offset', archived: 'archived' }, output: ['threads', 'next_offset'] },
createThread: { method: 'POST', path: '/agents/:slug/threads', body: ['thread_id', 'preview', 'expected_revision'], output: ['thread_id', 'client_thread_id'] },
history: { method: 'GET', path: '/threads/:thread_id', query: { offset: 'offset' }, output: ['thread_id', 'messages', 'run', 'queued_requests', 'schedule_proposals', 'next_offset'] },
history: { method: 'GET', path: '/threads/:thread_id', query: { offset: 'offset' }, output: ['thread_id', 'messages', 'run', 'queued_requests', 'schedule_proposals', 'next_offset'], project: projectHistory },
viewed: { method: 'POST', path: '/threads/:thread_id/viewed', body: ['run_id'], output: ['viewed'] },
submit: { method: 'POST', path: '/agents/:slug/requests', body: prompt, output: request },
preview: { method: 'POST', path: '/agents/:slug/preview', body: prompt, output: request },
+102 -16
View File
@@ -29,7 +29,12 @@ const MESSAGES: Record<string, string> = {
draft_revision_conflict: '草稿已在其他设备更新,你的输入已保留',
operation_conflict: '创建操作的内容发生冲突,请刷新列表确认',
cloud_service_unavailable: '智能体服务暂时不可用,请重试',
invalid_input: '请检查名称、用途和配置内容',
invalid_input: '提交的信息未被接受,请检查后再试',
invalid_name: '请填写名称,长度不要超过 100 个字',
invalid_introduction: '一句话介绍不符合要求,长度不要超过 2000 个字',
introduction_too_short: '一句话介绍不符合要求,请修改后再试',
introduction_required: '当前智能体服务尚不支持一句话介绍留空,请等待服务更新后重试',
invalid_icon: '头像设置未被接受,请重新选择头像',
model_required: '请先选择模型并保存',
model_unavailable: '所选模型当前不可用,请刷新模型目录',
resource_unavailable: '所选资源已不可用,请刷新配置',
@@ -73,11 +78,39 @@ const MESSAGES: Record<string, string> = {
};
export class CloudAgentsError extends Error {
constructor(readonly status: number, readonly code: string) {
constructor(readonly status: number, readonly code: string,
readonly commandOutcome: 'definitive_failure' | 'unknown' = 'unknown') {
super(MESSAGES[code] ?? MESSAGES.cloud_service_unavailable);
}
}
const validationTypes = new Set(['missing', 'string_type', 'string_too_short', 'string_too_long',
'string_pattern_mismatch', 'uuid_parsing', 'uuid_type', 'value_error']);
/** Only structural request-validation metadata is trusted; never expose msg, input or ctx. */
function requestValidation(detail: unknown): boolean {
return Array.isArray(detail) && detail.length > 0 && detail.length <= 100 && detail.every(value => {
const item = record(value);
return Array.isArray(item.loc) && item.loc[0] === 'body' && item.loc.length >= 2
&& typeof item.type === 'string' && validationTypes.has(item.type);
});
}
function validationMessageCode(detail: unknown): string {
if (!requestValidation(detail)) return 'invalid_input';
const codes = (detail as unknown[]).map(value => {
const item = record(value);
if (!Array.isArray(item.loc) || item.loc.length !== 2) return 'invalid_input';
switch (item.loc[1]) {
case 'name': return 'invalid_name';
case 'purpose': return item.type === 'string_too_short' ? 'introduction_too_short' : 'invalid_introduction';
case 'icon': return 'invalid_icon';
default: return 'invalid_input';
}
});
return codes.every(code => code === codes[0]) ? codes[0] : 'invalid_input';
}
function record(value: unknown): Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown> : {};
@@ -154,6 +187,7 @@ export class CloudAgentsModule {
private activityTimer: ReturnType<typeof setInterval>;
private activityPolling = false;
private activitySeen: Map<string, string> | null = null;
private readonly creationAttempts = new Map<string, { input: string; promise: Promise<CloudAgentDraft> }>();
constructor(private readonly fetchImpl: (input: string, init?: RequestInit) => Promise<Response> = proxyAwareFetch,
private readonly journal = new CloudAgentJournal()) {
@@ -196,17 +230,49 @@ export class CloudAgentsModule {
async create(value: unknown): Promise<CloudAgentDraft> {
const input = record(value);
const operationId = textField(input.operation_id, 36);
if (!/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(operationId)) {
throw new CloudAgentsError(422, 'invalid_input');
let body: CreateCloudAgent;
try {
const operationId = textField(input.operation_id, 36);
if (!/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(operationId)) {
throw new CloudAgentsError(422, 'invalid_input');
}
body = {
operation_id: operationId,
name: textField(input.name, 100).trim(),
purpose: textField(input.purpose, 2000, false).trim(),
...(input.icon === undefined ? {} : { icon: input.icon === null ? null : textField(input.icon, 255, false) }),
};
} catch (error) {
if (error instanceof CloudAgentsError) {
const binding = getWorksSquareAccountBinding();
const id = typeof input.operation_id === 'string' ? input.operation_id : '';
let alreadyPending = Boolean(binding && this.creationAttempts.has(`${binding.accountKey}:${binding.epoch}:${id}`));
if (binding) {
alreadyPending ||= (await this.journal.read(binding.accountKey)).pending.some(item => item.id === `createAgent::${id}`);
this.requireCurrent(binding);
}
throw new CloudAgentsError(error.status, error.code, alreadyPending ? 'unknown' : 'definitive_failure');
}
throw error;
}
const body: CreateCloudAgent = {
operation_id: operationId,
name: textField(input.name, 100).trim(),
purpose: textField(input.purpose, 2000, false).trim(),
...(input.icon === undefined ? {} : { icon: input.icon === null ? null : textField(input.icon, 255, false) }),
};
return this.withRecovery('createAgent', { ...body }, () => this.request('/api/makelore/agents', 'POST', body).then(result => this.withAvatar(projectDraft(result))));
const binding = getWorksSquareAccountBinding();
if (!binding) throw new CloudAgentsError(401, 'session_expired');
const key = `${binding.accountKey}:${binding.epoch}:${body.operation_id}`;
const serialized = JSON.stringify(body);
const existing = this.creationAttempts.get(key);
if (existing) {
if (existing.input !== serialized) throw new CloudAgentsError(409, 'operation_conflict');
return existing.promise;
}
const promise = this.withRecovery('createAgent', { ...body }, () => this.request('/api/makelore/agents', 'POST', body)
.then(result => this.withAvatar(projectDraft(result))).catch(error => {
if (body.purpose === '' && error instanceof CloudAgentsError && error.code === 'introduction_too_short') {
throw new CloudAgentsError(error.status, 'introduction_required', error.commandOutcome);
}
throw error;
})).finally(() => { this.creationAttempts.delete(key); });
this.creationAttempts.set(key, { input: serialized, promise });
return promise;
}
private async withRecovery<T>(operation: string, input: Record<string, unknown>, execute: () => Promise<T>): Promise<T> {
@@ -217,9 +283,23 @@ export class CloudAgentsModule {
.map(value => encodeURIComponent(typeof value === 'string' ? value : '')).join(':')
: input.request_id ?? input.operation_id;
const id = [operation, input.slug ?? input.application_id ?? input.run_id ?? '', operationKey].join(':');
await this.journal.put(binding.accountKey, { id, operation, input, created_at: new Date().toISOString() }, () => this.requireCurrent(binding));
const alreadyPending = await this.journal.put(binding.accountKey, { id, operation, input, created_at: new Date().toISOString() }, () => this.requireCurrent(binding));
this.requireCurrent(binding);
const result = await execute();
let result: T;
try {
result = await execute();
} catch (error) {
this.requireCurrent(binding);
if (error instanceof CloudAgentsError) {
// A retry rejection cannot disprove an earlier acceptance whose response was lost.
if (operation === 'createAgent' && !alreadyPending && error.commandOutcome === 'definitive_failure') {
await this.journal.remove(binding.accountKey, id, () => this.requireCurrent(binding));
throw error;
}
throw new CloudAgentsError(error.status, error.code, 'unknown');
}
throw error;
}
this.requireCurrent(binding);
await this.journal.remove(binding.accountKey, id, () => this.requireCurrent(binding));
return result;
@@ -671,11 +751,17 @@ export class CloudAgentsModule {
if (raw.length > 4_000_000) throw new CloudAgentsError(502, 'cloud_service_unavailable');
const body: unknown = JSON.parse(raw);
if (!response.ok) {
const code = record(record(body).detail).code;
const detail = record(body).detail;
const code = record(detail).code;
const rejected = ([400, 422].includes(response.status) && (code === 'invalid_input' || requestValidation(detail)))
|| (response.status === 401 && code === 'session_expired')
|| (response.status === 403 && ['access_revoked', 'module_unavailable'].includes(String(code)))
|| (response.status === 423 && code === 'account_locked');
throw new CloudAgentsError(
response.status, typeof code === 'string' && Object.hasOwn(MESSAGES, code) ? code
: response.status === 422 ? 'invalid_input' : response.status === 401 ? 'session_expired'
: response.status === 422 ? validationMessageCode(detail) : response.status === 401 ? 'session_expired'
: response.status === 403 ? 'access_revoked' : 'cloud_service_unavailable',
rejected ? 'definitive_failure' : 'unknown',
);
}
return body;