fix(coding): close PI-100 review findings

This commit is contained in:
2026-08-23 20:55:49 +08:00
parent 22b4a9f9c4
commit 52b2467d5d
16 changed files with 679 additions and 122 deletions

View File

@@ -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,

View 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');
}
}

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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: '编程操作失败,请重试。',
});
}