完善客户端模块与工作区能力
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
inman
2026-08-13 19:45:30 +08:00
parent 0148b91a15
commit 22add3f01f
97 changed files with 4756 additions and 3226 deletions

View File

@@ -2,6 +2,7 @@ import path from 'node:path';
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import {
createProjectConfig,
isProjectAgentAvatarDataUrl,
isProjectType,
validateAgentConfigs,
validateAgentNames,
@@ -61,6 +62,7 @@ function normalizeAgent(value: unknown): ProjectAgentConfig | null {
avatarId: typeof raw.avatarId === 'string' && /^avatar-(0[1-9]|1[0-6])$/.test(raw.avatarId)
? raw.avatarId
: 'avatar-01',
avatarDataUrl: isProjectAgentAvatarDataUrl(raw.avatarDataUrl) ? raw.avatarDataUrl : undefined,
roleName: typeof raw.roleName === 'string' && raw.roleName.trim() ? raw.roleName.trim() : '项目伙伴',
name,
// Preserve this legacy marker when reading an existing project. New
@@ -133,8 +135,10 @@ export async function readProjectConfig(projectPath: string): Promise<ProjectCon
try {
const raw = JSON.parse(await readFile(configPath(projectPath), 'utf8')) as unknown;
const config = normalizeProjectConfig(raw);
if (!(await areMaterializedAgentsCurrent(projectPath, config))) {
await materializeAgents(projectPath, config);
}
if (needsLegacyAgentModelMigration(raw)) {
if (config.initialized) await materializeAgents(projectPath, config);
await writeFile(configPath(projectPath), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
}
return { status: 'valid', config };
@@ -187,9 +191,12 @@ export function buildProjectAgentPrompt(config: ProjectConfig, current: ProjectA
}
function buildAgentMarkdown(config: ProjectConfig, agent: ProjectAgentConfig): string {
const skills = agent.skillIds.length > 0
? agent.skillIds.map((skill) => ` ${skill}: allow`).join('\n')
: ' "*": deny';
const skills = [
' "*": deny',
...agent.skillIds
.filter((skill) => skill !== '*')
.map((skill) => ` ${skill}: allow`),
].join('\n');
const shellPermission = agent.skillIds.includes('game-assets') ? ' bash: allow\n' : '';
const model = agent.model;
const prompt = agent.prompt.trim() || buildProjectAgentPrompt(config, agent);
@@ -205,6 +212,21 @@ ${skills}
${prompt}`;
}
async function areMaterializedAgentsCurrent(projectPath: string, config: ProjectConfig): Promise<boolean> {
if (!config.initialized || config.agents.length === 0) return true;
const results = await Promise.all(config.agents.map(async (agent) => {
try {
const existing = await readFile(path.join(projectPath, '.opencode', 'agent', `${agent.id}.md`), 'utf8');
return existing === buildAgentMarkdown(config, agent);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
throw error;
}
}));
return results.every(Boolean);
}
async function materializeAgents(projectPath: string, config: ProjectConfig): Promise<void> {
const agentDirectory = path.join(projectPath, '.opencode', 'agent');
await mkdir(agentDirectory, { recursive: true });