feat: prepare Zhinian desktop client for pilot release

This commit is contained in:
inman
2026-04-29 10:23:20 +08:00
parent f9361e686a
commit 47b83b79fc
149 changed files with 15341 additions and 3590 deletions

View File

@@ -12,6 +12,25 @@ const MAIN_AGENT_ID = 'main';
const MAIN_AGENT_NAME = 'Main Agent';
const DEFAULT_ACCOUNT_ID = 'default';
const DEFAULT_WORKSPACE_PATH = '~/.openclaw/workspace';
const CHANNEL_AGENT_LABELS: Record<string, string> = {
wechat: '微信',
wecom: '企业微信',
dingtalk: '钉钉',
feishu: '飞书',
lark: '飞书',
telegram: 'Telegram',
whatsapp: 'WhatsApp',
discord: 'Discord',
signal: 'Signal',
imessage: 'iMessage',
matrix: 'Matrix',
line: 'LINE',
msteams: 'Teams',
googlechat: 'Google Chat',
mattermost: 'Mattermost',
qqbot: 'QQ',
agentbus: 'AgentBus',
};
const AGENT_BOOTSTRAP_FILES = [
'AGENTS.md',
'SOUL.md',
@@ -144,6 +163,30 @@ function slugifyAgentId(name: string): string {
return normalized;
}
function normalizeChannelAgentIdSegment(value: string): string {
const normalized = value
.trim()
.toLowerCase()
.replace(/[^a-z0-9_-]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.slice(0, 48);
return normalized || 'default';
}
function buildChannelAgentIdentity(channelType: string, accountId: string): { id: string; name: string } {
const uiChannelType = toUiChannelType(channelType);
const channelSegment = normalizeChannelAgentIdSegment(uiChannelType);
const accountSegment = normalizeChannelAgentIdSegment(accountId || DEFAULT_ACCOUNT_ID);
const label = CHANNEL_AGENT_LABELS[uiChannelType] ?? uiChannelType;
const isDefaultAccount = accountSegment === DEFAULT_ACCOUNT_ID;
return {
id: isDefaultAccount ? `channel-${channelSegment}` : `channel-${channelSegment}-${accountSegment}`,
name: isDefaultAccount ? `${label}助手` : `${label}助手 ${accountId}`,
};
}
async function fileExists(path: string): Promise<boolean> {
try {
await access(path, constants.F_OK);
@@ -775,6 +818,54 @@ export async function assignChannelAccountToAgent(
});
}
export async function ensureChannelAgentForAccount(
channelType: string,
accountId = DEFAULT_ACCOUNT_ID,
): Promise<AgentsSnapshot> {
return withConfigLock(async () => {
const normalizedAccountId = accountId.trim() || DEFAULT_ACCOUNT_ID;
const config = await readOpenClawConfig() as AgentConfigDocument;
const { agentsConfig, entries, syntheticMain } = normalizeAgentsConfig(config);
const identity = buildChannelAgentIdentity(channelType, normalizedAccountId);
const nextEntries = syntheticMain
? [createImplicitMainEntry(config), ...entries.filter((_, index) => index > 0)]
: [...entries];
let agentEntry = nextEntries.find((entry) => entry.id === identity.id);
if (!agentEntry) {
agentEntry = {
id: identity.id,
name: identity.name,
workspace: `~/.openclaw/workspace-${identity.id}`,
agentDir: getDefaultAgentDirPath(identity.id),
};
nextEntries.push(agentEntry);
await provisionAgentFilesystem(config, agentEntry, { inheritWorkspace: true });
logger.info('Created channel agent config entry', {
agentId: identity.id,
channelType,
accountId: normalizedAccountId,
});
} else if (!agentEntry.name) {
agentEntry.name = identity.name;
}
config.agents = {
...agentsConfig,
list: nextEntries,
};
config.bindings = upsertBindingsForChannel(config.bindings, channelType, identity.id, normalizedAccountId);
await writeOpenClawConfig(config);
logger.info('Ensured channel agent binding', {
agentId: identity.id,
channelType,
accountId: normalizedAccountId,
});
return buildSnapshotFromConfig(config);
});
}
export async function clearChannelBinding(channelType: string, accountId?: string): Promise<AgentsSnapshot> {
return withConfigLock(async () => {
const config = await readOpenClawConfig() as AgentConfigDocument;