build(trial): preserve deployed source for knowledge refresh
This commit is contained in:
1 parent
8e0e52f30e
commit
2d4e44f4a0
42 files changed
+2321
-283
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user