fix(coding): close PI-100 review findings
This commit is contained in:
@@ -25,7 +25,12 @@ import { createPiManagedSubagentChildOpener } from '../coding-runtime/pi/subagen
|
||||
import { PiSubagentScheduler } from '../coding-runtime/pi/subagent';
|
||||
import { PiProcessBudget, PiWorkerPool } from '../coding-runtime/pi/worker-pool';
|
||||
import { getProviderService } from '../services/providers/provider-service';
|
||||
import {
|
||||
isCodingProviderAuthenticationError,
|
||||
refreshCodingProviderCredential,
|
||||
} from './coding-provider-auth';
|
||||
import { createCodingProductHost, type CodingProductComposition } from './coding-product-services';
|
||||
import { archivePiConversationSession } from '../coding-runtime/pi/resource-loader';
|
||||
|
||||
export interface CodingCompositionPaths {
|
||||
executablePath: string;
|
||||
@@ -128,6 +133,8 @@ export function createCodingComposition(
|
||||
buildPiProviderCatalog(await loadProviderInput()),
|
||||
model,
|
||||
),
|
||||
isAuthenticationError: isCodingProviderAuthenticationError,
|
||||
refreshCredential: refreshCodingProviderCredential,
|
||||
resolveImages: async (refs) => await Promise.all(refs.map(async ({ attachmentId }) => {
|
||||
const record = await attachments.read(attachmentId);
|
||||
return {
|
||||
@@ -155,7 +162,15 @@ export function createCodingComposition(
|
||||
]);
|
||||
},
|
||||
});
|
||||
const conversations = new CodingConversationService(projects, runtime);
|
||||
const conversations = new CodingConversationService(projects, runtime, {
|
||||
archiveSession: async ({ projectId, sessionKey }) => {
|
||||
await archivePiConversationSession({
|
||||
userDataDir: options.paths.userDataDir,
|
||||
projectId,
|
||||
sessionKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
const host = createCodingProductHost({
|
||||
projects,
|
||||
productTools,
|
||||
|
||||
40
electron/api/coding-provider-auth.ts
Normal file
40
electron/api/coding-provider-auth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { resolvePiProviderCredentialFromSecretStore } from '../coding-runtime/pi/provider-config';
|
||||
import { getProviderService } from '../services/providers/provider-service';
|
||||
import {
|
||||
getFreshWorksSquareAIGatewayCredential,
|
||||
markWorksSquareAIGatewayCredentialExpired,
|
||||
} from '../services/works-square-ai-gateway';
|
||||
|
||||
const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway';
|
||||
const AUTHENTICATION_ERROR_PATTERN = /\b(?:401|403|unauthori[sz]ed|forbidden|authentication failed|auth failed|invalid (?:api key|credential|access token|bearer token)|(?:access |bearer )?token expired)\b/i;
|
||||
|
||||
export function isCodingProviderAuthenticationError(error: unknown): boolean {
|
||||
return error instanceof Error && AUTHENTICATION_ERROR_PATTERN.test(error.message);
|
||||
}
|
||||
|
||||
export async function refreshCodingProviderCredential(accountId: string): Promise<void> {
|
||||
const providerService = getProviderService();
|
||||
const account = await providerService.getAccount(accountId);
|
||||
if (!account?.enabled) throw new Error('Provider account is unavailable');
|
||||
|
||||
if (account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE) {
|
||||
markWorksSquareAIGatewayCredentialExpired();
|
||||
const credential = await getFreshWorksSquareAIGatewayCredential();
|
||||
if (!credential) throw new Error('Provider credential refresh failed');
|
||||
await providerService.updateAccount(account.id, {
|
||||
baseUrl: credential.oneApiBaseUrl,
|
||||
metadata: {
|
||||
...account.metadata,
|
||||
worksSquareCredentialExpiresAt: credential.expiresAt === null
|
||||
? undefined
|
||||
: new Date(credential.expiresAt).toISOString(),
|
||||
},
|
||||
}, credential.accessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
const current = await resolvePiProviderCredentialFromSecretStore(account);
|
||||
if (!current && account.authMode !== 'local') {
|
||||
throw new Error('Provider credential is unavailable');
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export async function handleCodingConversationRoutes(
|
||||
sendJson(res, 503, {
|
||||
success: false,
|
||||
code: 'CODING_CORE_UNAVAILABLE',
|
||||
error: 'Coding services are unavailable',
|
||||
error: '本地编程服务暂时不可用。',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,33 +2,22 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { CodingProductHostError } from '../coding-product-services';
|
||||
import { sendJson } from '../route-utils';
|
||||
import { sendFixedCodingError } from './coding-route-errors';
|
||||
|
||||
function unavailable(res: ServerResponse): void {
|
||||
sendJson(res, 503, {
|
||||
success: false,
|
||||
code: 'CODING_PRODUCT_TOOLS_UNAVAILABLE',
|
||||
error: 'Coding product tools are unavailable',
|
||||
});
|
||||
sendFixedCodingError(res, 503, 'CODING_PRODUCT_TOOLS_UNAVAILABLE');
|
||||
}
|
||||
|
||||
function serviceError(res: ServerResponse, error: unknown): void {
|
||||
if (error instanceof CodingProductHostError) {
|
||||
sendJson(res, error.status, {
|
||||
success: false,
|
||||
code: error.code,
|
||||
error: error.message,
|
||||
});
|
||||
sendFixedCodingError(res, error.status, error.code);
|
||||
return;
|
||||
}
|
||||
const code = error && typeof error === 'object' && 'code' in error
|
||||
? String(error.code)
|
||||
: '';
|
||||
if (code === 'ENOENT') {
|
||||
sendJson(res, 404, {
|
||||
success: false,
|
||||
code: 'CODING_FILE_NOT_FOUND',
|
||||
error: 'Project file does not exist',
|
||||
});
|
||||
sendFixedCodingError(res, 404, 'CODING_FILE_NOT_FOUND');
|
||||
return;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : '';
|
||||
@@ -44,18 +33,10 @@ function serviceError(res: ServerResponse, error: unknown): void {
|
||||
'Project file is not valid UTF-8 text',
|
||||
]);
|
||||
if (knownInputError.has(message)) {
|
||||
sendJson(res, 400, {
|
||||
success: false,
|
||||
code: 'CODING_FILE_REQUEST_INVALID',
|
||||
error: message,
|
||||
});
|
||||
sendFixedCodingError(res, 400, 'CODING_FILE_REQUEST_INVALID');
|
||||
return;
|
||||
}
|
||||
sendJson(res, 500, {
|
||||
success: false,
|
||||
code: 'CODING_PRODUCT_TOOL_FAILED',
|
||||
error: 'Coding product request failed',
|
||||
});
|
||||
sendFixedCodingError(res, 500, 'CODING_PRODUCT_TOOL_FAILED');
|
||||
}
|
||||
|
||||
export async function handleCodingFileRoutes(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import type { ProjectType } from '../../../shared/project-config';
|
||||
import type { CodingProjectConfigSnapshot } from '../../coding-projects/project-service';
|
||||
import type { CodingProject } from '../../coding-projects/project-store';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson, sendNoContent } from '../route-utils';
|
||||
import { sendCodingRouteError } from './coding-route-errors';
|
||||
@@ -9,6 +11,19 @@ function isProjectRoute(pathname: string): boolean {
|
||||
|| pathname.startsWith('/api/coding/projects/');
|
||||
}
|
||||
|
||||
function publicProject(project: CodingProject | null): Omit<CodingProject, 'path'> | null {
|
||||
if (!project) return null;
|
||||
const { path: _path, ...safe } = project;
|
||||
return safe;
|
||||
}
|
||||
|
||||
function publicProjectSnapshot(snapshot: CodingProjectConfigSnapshot) {
|
||||
return {
|
||||
...snapshot,
|
||||
project: publicProject(snapshot.project),
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleCodingProjectRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
@@ -22,7 +37,7 @@ export async function handleCodingProjectRoutes(
|
||||
sendJson(res, 503, {
|
||||
success: false,
|
||||
code: 'CODING_CORE_UNAVAILABLE',
|
||||
error: 'Coding services are unavailable',
|
||||
error: '本地编程服务暂时不可用。',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -33,12 +48,15 @@ export async function handleCodingProjectRoutes(
|
||||
projects.listProjects(),
|
||||
projects.getActiveProject(),
|
||||
]);
|
||||
sendJson(res, 200, { projects: items, activeProjectId: activeProject?.id ?? null });
|
||||
sendJson(res, 200, {
|
||||
projects: items.map((project) => publicProject(project)),
|
||||
activeProjectId: activeProject?.id ?? null,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === '/api/coding/projects/open' && req.method === 'POST') {
|
||||
const body = await parseJsonBody<{ projectPath?: string }>(req);
|
||||
sendJson(res, 200, { project: await projects.openProject(body.projectPath ?? '') });
|
||||
sendJson(res, 200, { project: publicProject(await projects.openProject(body.projectPath ?? '')) });
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === '/api/coding/projects/create' && req.method === 'POST') {
|
||||
@@ -48,7 +66,7 @@ export async function handleCodingProjectRoutes(
|
||||
projectName?: string;
|
||||
projectType?: ProjectType;
|
||||
}>(req);
|
||||
sendJson(res, 201, { snapshot: await projects.createProject(body) });
|
||||
sendJson(res, 201, { snapshot: publicProjectSnapshot(await projects.createProject(body)) });
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === '/api/coding/projects/remove' && req.method === 'POST') {
|
||||
@@ -58,24 +76,26 @@ export async function handleCodingProjectRoutes(
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === '/api/coding/projects/active' && req.method === 'GET') {
|
||||
sendJson(res, 200, { project: await projects.getActiveProject() });
|
||||
sendJson(res, 200, { project: publicProject(await projects.getActiveProject()) });
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === '/api/coding/projects/active' && req.method === 'POST') {
|
||||
const body = await parseJsonBody<{ projectId?: string }>(req);
|
||||
sendJson(res, 200, { project: await projects.setActiveProject(body.projectId ?? '') });
|
||||
sendJson(res, 200, { project: publicProject(await projects.setActiveProject(body.projectId ?? '')) });
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === '/api/coding/projects/config' && req.method === 'GET') {
|
||||
sendJson(res, 200, {
|
||||
snapshot: await projects.getConfig(url.searchParams.get('projectId')?.trim() || undefined),
|
||||
snapshot: publicProjectSnapshot(
|
||||
await projects.getConfig(url.searchParams.get('projectId')?.trim() || undefined),
|
||||
),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (url.pathname === '/api/coding/projects/config' && req.method === 'PUT') {
|
||||
const body = await parseJsonBody<{ projectId?: string; config?: unknown }>(req);
|
||||
sendJson(res, 200, {
|
||||
snapshot: await projects.saveConfig(body.projectId ?? '', body.config),
|
||||
snapshot: publicProjectSnapshot(await projects.saveConfig(body.projectId ?? '', body.config)),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,27 +3,72 @@ import { CodingProjectServiceError } from '../../coding-projects/project-service
|
||||
import { CodingConversationServiceError } from '../../coding-runtime/conversation-service';
|
||||
import { sendJson } from '../route-utils';
|
||||
|
||||
const FIXED_CODING_ERROR_MESSAGES: Readonly<Record<string, string>> = {
|
||||
CODING_ACTIVE_PROJECT_REQUIRED: '请先选择一个编程项目。',
|
||||
CODING_AGENT_NOT_FOUND: '当前伙伴不可用,请重新选择。',
|
||||
CODING_AGENT_ID_IMMUTABLE: '已有伙伴标识不能修改。',
|
||||
CODING_CONVERSATION_NOT_FOUND: '指定的对话不存在。',
|
||||
CODING_CONVERSATION_REQUEST_INVALID: '对话请求无效,请检查输入。',
|
||||
CODING_FILE_NOT_FOUND: '指定的项目文件不存在。',
|
||||
CODING_FILE_REQUEST_INVALID: '文件请求无效,请检查输入。',
|
||||
CODING_INTERACTION_REQUEST_INVALID: '交互响应无效,请重试。',
|
||||
CODING_KNOWLEDGE_ALREADY_EXISTS: '同名知识文件已存在。',
|
||||
CODING_KNOWLEDGE_REQUEST_INVALID: '知识文件无效,请检查后重试。',
|
||||
CODING_MIGRATION_MODEL_REQUIRED: '请先为该对话选择一个可用模型。',
|
||||
CODING_MODEL_UNAVAILABLE: '所选模型当前不可用,请重新选择。',
|
||||
CODING_PROJECT_ALREADY_EXISTS: '该编程项目已经存在。',
|
||||
CODING_PROJECT_CONFIG_INVALID: '项目配置无效,请检查后重试。',
|
||||
CODING_PROJECT_IDENTITY_IMMUTABLE: '项目创建标识不能修改。',
|
||||
CODING_PROJECT_NOT_FOUND: '指定的编程项目不存在。',
|
||||
CODING_PROJECT_REQUEST_INVALID: '项目请求无效,请检查输入。',
|
||||
CODING_PROJECT_TYPE_IMMUTABLE: '项目类型创建后不能修改。',
|
||||
CODING_PRODUCT_TOOL_FAILED: '项目工具执行失败,请重试。',
|
||||
CODING_PRODUCT_TOOLS_UNAVAILABLE: '项目工具暂时不可用。',
|
||||
CODING_PROVIDER_AUTH_REQUIRED: 'Provider 凭证不可用,请修复账号后重试。',
|
||||
CODING_REQUEST_CAPACITY_EXCEEDED: '本地请求队列已满,请稍后重试。',
|
||||
CODING_REQUEST_ID_CONFLICT: '该请求标识已用于不同内容,请使用新的标识。',
|
||||
CODING_REQUEST_UNCERTAIN: '请求状态无法确认,请先核对对话后再决定是否重试。',
|
||||
CODING_RUNTIME_PROTOCOL_ERROR: '本地 Agent 通信异常,请执行恢复。',
|
||||
CODING_RUNTIME_READY_TIMEOUT: '本地 Agent 启动超时,请执行恢复。',
|
||||
CODING_RUNTIME_START_FAILED: '本地 Agent 启动失败,请执行恢复。',
|
||||
CODING_RUNTIME_UNAVAILABLE: '本地编程运行时暂时不可用。',
|
||||
CODING_SESSION_UNREADABLE: '对话会话无法读取,原文件已保留。',
|
||||
CODING_STORAGE_WRITE_FAILED: '本地数据写入失败,请检查存储后重试。',
|
||||
};
|
||||
|
||||
function fixedMessage(code: string): string {
|
||||
return FIXED_CODING_ERROR_MESSAGES[code] ?? '编程操作失败,请重试。';
|
||||
}
|
||||
|
||||
export function sendFixedCodingError(
|
||||
res: ServerResponse,
|
||||
status: number,
|
||||
code: string,
|
||||
): void {
|
||||
sendJson(res, status, {
|
||||
success: false,
|
||||
code,
|
||||
error: fixedMessage(code),
|
||||
});
|
||||
}
|
||||
|
||||
export function sendCodingRouteError(res: ServerResponse, error: unknown): void {
|
||||
if (error instanceof CodingProjectServiceError || error instanceof CodingConversationServiceError) {
|
||||
sendJson(res, error.status, {
|
||||
success: false,
|
||||
code: error.code,
|
||||
error: error.message,
|
||||
});
|
||||
sendFixedCodingError(res, error.status, error.code);
|
||||
return;
|
||||
}
|
||||
if (error instanceof SyntaxError) {
|
||||
sendJson(res, 400, {
|
||||
success: false,
|
||||
code: 'CODING_REQUEST_INVALID',
|
||||
error: 'Request JSON is invalid',
|
||||
error: '请求内容无效,请检查后重试。',
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendJson(res, 500, {
|
||||
success: false,
|
||||
code: 'CODING_REQUEST_FAILED',
|
||||
error: 'Coding request failed',
|
||||
error: '编程操作失败,请重试。',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user