feat: remove legacy OpenCode runtime

Cut product flows over to Coding/Pi and retain only the migration-owned v1 boundary. Promote supported native optional packages because electron-builder omitted pnpm transitive optional closure from the packaged ASAR.
This commit is contained in:
2026-08-24 12:17:43 +08:00
parent 61fede2b4d
commit 5a275b93a7
199 changed files with 1330 additions and 64725 deletions

View File

@@ -2,10 +2,15 @@ import { useStore } from 'zustand';
import { createStore, type StoreApi } from 'zustand/vanilla';
import {
createCodingProjectConversation,
createCodingProject,
getCodingProjectConfig,
listCodingProjectConversations,
listCodingProjects,
acknowledgeLegacyCodingConversationNotice,
openCodingProject,
patchCodingProjectConversation,
removeCodingProject,
setActiveCodingProject,
type CodingProjectCatalog,
} from '@/lib/coding-projects';
import type {
@@ -14,6 +19,7 @@ import type {
CodingProjectConfig,
CodingProjectSummary,
} from '@/types/coding-project';
import type { ProjectType } from '../../shared/project-config';
interface CodingWorkspaceDependencies {
listProjects(): Promise<CodingProjectCatalog>;
@@ -31,6 +37,20 @@ interface CodingWorkspaceDependencies {
conversationId: string,
patch: { title?: string; archived?: boolean; unread?: boolean },
): Promise<CodingConversationMetadata>;
openProject(projectPath: string): Promise<CodingProjectSummary>;
createProject(input: {
projectPath?: string;
parentPath?: string;
projectName?: string;
projectType?: ProjectType;
}): Promise<{ project: CodingProjectSummary; config: CodingProjectConfig; knowledgeFiles: string[] }>;
setActiveProject(projectId: string): Promise<CodingProjectSummary>;
removeProject(projectId: string): Promise<void>;
acknowledgeLegacyNotice(projectId: string): Promise<{
project: CodingProjectSummary;
config: CodingProjectConfig;
knowledgeFiles: string[];
}>;
}
export interface CodingWorkspaceState {
@@ -45,6 +65,16 @@ export interface CodingWorkspaceState {
conversationErrorsByProjectId: Record<string, Record<string, string>>;
creatingAgentIds: Record<string, true>;
load(): Promise<void>;
openProject(projectPath: string): Promise<CodingProjectSummary>;
createProject(input: {
projectPath?: string;
parentPath?: string;
projectName?: string;
projectType?: ProjectType;
}): Promise<CodingProjectSummary>;
setActiveProject(projectId: string): Promise<CodingProjectSummary>;
removeProject(projectId: string): Promise<void>;
acknowledgeLegacyNotice(): Promise<void>;
selectAgent(agentId: string): void;
ensureConversation(agentId: string): Promise<CodingConversationMetadata>;
createConversation(agentId: string): Promise<CodingConversationMetadata>;
@@ -99,6 +129,11 @@ function defaultDependencies(): CodingWorkspaceDependencies {
listConversations: listCodingProjectConversations,
createConversation: createCodingProjectConversation,
patchConversation: patchCodingProjectConversation,
openProject: openCodingProject,
createProject: createCodingProject,
setActiveProject: setActiveCodingProject,
removeProject: removeCodingProject,
acknowledgeLegacyNotice: acknowledgeLegacyCodingConversationNotice,
};
}
@@ -177,6 +212,36 @@ export function createCodingWorkspaceStore(
await flight;
},
async openProject(projectPath) {
const project = await deps.openProject(projectPath);
await get().load();
return project;
},
async createProject(input) {
const snapshot = await deps.createProject(input);
await get().load();
return snapshot.project;
},
async setActiveProject(projectId) {
const project = await deps.setActiveProject(projectId);
await get().load();
return project;
},
async removeProject(projectId) {
await deps.removeProject(projectId);
await get().load();
},
async acknowledgeLegacyNotice() {
const projectId = get().activeProjectId;
if (!projectId) return;
const snapshot = await deps.acknowledgeLegacyNotice(projectId);
if (get().activeProjectId === projectId) set({ config: snapshot.config });
},
selectAgent(agentId) {
if (!enabledAgent(get().config, agentId)) return;
set({ selectedAgentId: agentId });

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,21 @@
import { create } from 'zustand';
import { hostApiFetch } from '@/lib/host-api';
import type { ProjectConfig } from '../../shared/project-config';
import type { CodingProjectConfig, CodingProjectConfigSnapshot } from '@/types/coding-project';
type ProjectConfigResponse = {
status: 'valid' | 'missing' | 'invalid';
config?: ProjectConfig;
config?: CodingProjectConfig;
error?: string;
knowledgeFiles: string[];
};
type ProjectConfigState = {
configsByProjectId: Record<string, ProjectConfig>;
configsByProjectId: Record<string, CodingProjectConfig>;
knowledgeByProjectId: Record<string, string[]>;
loadingProjectId: string | null;
errorsByProjectId: Record<string, string>;
load: (projectId: string) => Promise<ProjectConfigResponse>;
save: (projectId: string, config: ProjectConfig) => Promise<ProjectConfig>;
save: (projectId: string, config: CodingProjectConfig) => Promise<CodingProjectConfig>;
uploadKnowledge: (projectId: string, file: File) => Promise<void>;
remove: (projectId: string) => void;
};
@@ -29,18 +29,21 @@ export const useProjectConfigStore = create<ProjectConfigState>((set) => ({
async load(projectId) {
set({ loadingProjectId: projectId });
try {
const response = await hostApiFetch<ProjectConfigResponse>(`/api/opencode/projects/config?projectId=${encodeURIComponent(projectId)}`);
const response = await hostApiFetch<{ snapshot: CodingProjectConfigSnapshot }>(
`/api/coding/projects/config?projectId=${encodeURIComponent(projectId)}`,
);
const result: ProjectConfigResponse = {
status: 'valid',
config: response.snapshot.config,
knowledgeFiles: response.snapshot.knowledgeFiles,
};
set((state) => ({
loadingProjectId: null,
configsByProjectId: response.status === 'valid' && response.config
? { ...state.configsByProjectId, [projectId]: response.config }
: state.configsByProjectId,
knowledgeByProjectId: { ...state.knowledgeByProjectId, [projectId]: response.knowledgeFiles ?? [] },
errorsByProjectId: response.status === 'invalid'
? { ...state.errorsByProjectId, [projectId]: response.error ?? '项目配置无效' }
: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
configsByProjectId: { ...state.configsByProjectId, [projectId]: response.snapshot.config },
knowledgeByProjectId: { ...state.knowledgeByProjectId, [projectId]: response.snapshot.knowledgeFiles },
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
}));
return response;
return result;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
set((state) => ({ loadingProjectId: null, errorsByProjectId: { ...state.errorsByProjectId, [projectId]: message } }));
@@ -49,17 +52,16 @@ export const useProjectConfigStore = create<ProjectConfigState>((set) => ({
},
async save(projectId, config) {
const response = await hostApiFetch<{ success: boolean; config?: ProjectConfig; knowledgeFiles?: string[]; error?: string }>('/api/opencode/projects/config', {
const response = await hostApiFetch<{ snapshot: CodingProjectConfigSnapshot }>('/api/coding/projects/config', {
method: 'PUT',
body: JSON.stringify({ projectId, config }),
});
if (!response.success || !response.config) throw new Error(response.error || '保存项目配置失败');
set((state) => ({
configsByProjectId: { ...state.configsByProjectId, [projectId]: response.config as ProjectConfig },
knowledgeByProjectId: { ...state.knowledgeByProjectId, [projectId]: response.knowledgeFiles ?? [] },
configsByProjectId: { ...state.configsByProjectId, [projectId]: response.snapshot.config },
knowledgeByProjectId: { ...state.knowledgeByProjectId, [projectId]: response.snapshot.knowledgeFiles },
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
}));
return response.config;
return response.snapshot.config;
},
async uploadKnowledge(projectId, file) {
@@ -68,12 +70,11 @@ export const useProjectConfigStore = create<ProjectConfigState>((set) => ({
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
}
const response = await hostApiFetch<{ success: boolean; knowledgeFiles?: string[]; error?: string }>('/api/opencode/projects/knowledge', {
const response = await hostApiFetch<{ knowledgeFiles: string[] }>('/api/coding/projects/knowledge', {
method: 'POST',
body: JSON.stringify({ projectId, fileName: file.name, contentBase64: btoa(binary) }),
});
if (!response.success) throw new Error(response.error || '知识文件上传失败');
set((state) => ({ knowledgeByProjectId: { ...state.knowledgeByProjectId, [projectId]: response.knowledgeFiles ?? [] } }));
set((state) => ({ knowledgeByProjectId: { ...state.knowledgeByProjectId, [projectId]: response.knowledgeFiles } }));
},
remove(projectId) {

View File

@@ -1,117 +0,0 @@
import { create } from 'zustand';
import { hostApiFetch } from '@/lib/host-api';
import type {
ProjectConversationState,
ProjectSessionMetadata,
} from '../../shared/project-conversations';
type ProjectConversationResponse = {
state?: ProjectConversationState;
success?: boolean;
error?: string;
};
type ConversationAction = 'link' | 'archive' | 'restore' | 'delete' | 'read' | 'increment-unread' | 'complete';
type ProjectConversationStore = {
statesByProjectId: Record<string, ProjectConversationState>;
loadingProjectId: string | null;
errorsByProjectId: Record<string, string>;
load: (projectId: string) => Promise<ProjectConversationState>;
linkSession: (projectId: string, sessionId: string, agentId: string) => Promise<ProjectConversationState>;
archiveSession: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
restoreSession: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
deleteSession: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
markSessionRead: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
markSessionUnread: (projectId: string, sessionId: string) => Promise<ProjectConversationState>;
completeSession: (projectId: string, sessionId: string, incrementUnread?: boolean) => Promise<ProjectConversationState>;
getSession: (projectId: string, sessionId: string) => ProjectSessionMetadata | undefined;
remove: (projectId: string) => void;
};
function emptyState(): ProjectConversationState {
return { schemaVersion: 1, sessions: [], updatedAt: new Date().toISOString() };
}
export const useProjectConversationStore = create<ProjectConversationStore>((set, get) => {
async function dispatch(projectId: string, sessionId: string, action: ConversationAction, agentId?: string, unread?: boolean) {
const response = await hostApiFetch<ProjectConversationResponse>('/api/opencode/projects/conversations', {
method: 'POST',
body: JSON.stringify({ projectId, sessionId, action, ...(agentId ? { agentId } : {}), ...(unread ? { unread: true } : {}) }),
});
if (!response.success || !response.state) throw new Error(response.error || '更新会话归属失败');
set((state) => ({
statesByProjectId: { ...state.statesByProjectId, [projectId]: response.state as ProjectConversationState },
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
}));
return response.state;
}
return {
statesByProjectId: {},
loadingProjectId: null,
errorsByProjectId: {},
async load(projectId) {
set({ loadingProjectId: projectId });
try {
const response = await hostApiFetch<ProjectConversationResponse>(
`/api/opencode/projects/conversations?projectId=${encodeURIComponent(projectId)}`,
);
const nextState = response.state ?? emptyState();
set((state) => ({
loadingProjectId: null,
statesByProjectId: { ...state.statesByProjectId, [projectId]: nextState },
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
}));
return nextState;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
set((state) => ({
loadingProjectId: null,
errorsByProjectId: { ...state.errorsByProjectId, [projectId]: message },
}));
throw error;
}
},
linkSession(projectId, sessionId, agentId) {
return dispatch(projectId, sessionId, 'link', agentId);
},
archiveSession(projectId, sessionId) {
return dispatch(projectId, sessionId, 'archive');
},
restoreSession(projectId, sessionId) {
return dispatch(projectId, sessionId, 'restore');
},
deleteSession(projectId, sessionId) {
return dispatch(projectId, sessionId, 'delete');
},
markSessionRead(projectId, sessionId) {
return dispatch(projectId, sessionId, 'read');
},
markSessionUnread(projectId, sessionId) {
return dispatch(projectId, sessionId, 'increment-unread');
},
completeSession(projectId, sessionId, incrementUnread = false) {
return dispatch(projectId, sessionId, 'complete', undefined, incrementUnread);
},
getSession(projectId, sessionId) {
return get().statesByProjectId[projectId]?.sessions.find((item) => item.sessionId === sessionId);
},
remove(projectId) {
set((state) => ({
statesByProjectId: Object.fromEntries(Object.entries(state.statesByProjectId).filter(([id]) => id !== projectId)),
errorsByProjectId: Object.fromEntries(Object.entries(state.errorsByProjectId).filter(([id]) => id !== projectId)),
}));
},
};
});