feat: prepare Zhinian desktop client for pilot release
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1112,7 +1112,26 @@ export function listConfiguredChannelAccountsFromConfig(config: OpenClawConfig):
|
||||
|
||||
const accounts = getChannelAccountsMap(section);
|
||||
const accountIds = accounts
|
||||
? Object.keys(accounts).filter((accountId) => accountId.trim().length > 0)
|
||||
? Object.keys(accounts).filter((accountId) => {
|
||||
if (!accountId.trim()) return false;
|
||||
if (channelType !== 'agentbus' || accountId !== DEFAULT_ACCOUNT_ID) return true;
|
||||
const configuredDefault = typeof section.defaultAccount === 'string' ? section.defaultAccount.trim() : '';
|
||||
const accountConfig = accounts[accountId];
|
||||
// AgentBus stores legacy top-level connection fields and may mirror a
|
||||
// partial `accounts.default` object during generic edits. Treat that
|
||||
// mirror as display/routing noise unless it is explicitly the default
|
||||
// account and contains the fields a real AgentBus account needs.
|
||||
if (configuredDefault && configuredDefault !== DEFAULT_ACCOUNT_ID) return false;
|
||||
return Boolean(
|
||||
accountConfig
|
||||
&& typeof accountConfig.id === 'string'
|
||||
&& accountConfig.id.trim()
|
||||
&& typeof accountConfig.agentId === 'string'
|
||||
&& accountConfig.agentId.trim()
|
||||
&& typeof accountConfig.wsUrl === 'string'
|
||||
&& accountConfig.wsUrl.trim(),
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
let defaultAccountId = typeof section.defaultAccount === 'string' && section.defaultAccount.trim()
|
||||
|
||||
@@ -28,7 +28,9 @@ export interface DeviceAuthPayloadParams {
|
||||
signedAtMs: number;
|
||||
token?: string | null;
|
||||
nonce?: string | null;
|
||||
version?: 'v1' | 'v2';
|
||||
version?: 'v1' | 'v2' | 'v3';
|
||||
platform?: string | null;
|
||||
deviceFamily?: string | null;
|
||||
}
|
||||
|
||||
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
||||
@@ -143,5 +145,10 @@ export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string
|
||||
token,
|
||||
];
|
||||
if (version === 'v2') base.push(params.nonce ?? '');
|
||||
if (version === 'v3') {
|
||||
base.push(params.nonce ?? '');
|
||||
base.push(params.platform?.trim() ?? '');
|
||||
base.push(params.deviceFamily?.trim() ?? '');
|
||||
}
|
||||
return base.join('|');
|
||||
}
|
||||
|
||||
@@ -3,13 +3,25 @@
|
||||
* Cross-platform path resolution helpers
|
||||
*/
|
||||
import { createRequire } from 'node:module';
|
||||
import { join } from 'path';
|
||||
import { dirname, isAbsolute, join, normalize } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'fs';
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync } from 'fs';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
type ElectronAppLike = Pick<typeof import('electron').app, 'isPackaged' | 'getPath' | 'getAppPath'>;
|
||||
type OpenClawRuntimeSource = 'dev' | 'external' | 'managed' | 'bundled' | 'missing';
|
||||
|
||||
type OpenClawRuntimeResolution = {
|
||||
dir: string;
|
||||
source: OpenClawRuntimeSource;
|
||||
version?: string;
|
||||
bundledDir?: string;
|
||||
managedDir?: string;
|
||||
installedFromBundled?: boolean;
|
||||
};
|
||||
|
||||
let cachedOpenClawRuntime: OpenClawRuntimeResolution | null = null;
|
||||
|
||||
export {
|
||||
quoteForCmd,
|
||||
@@ -91,6 +103,218 @@ export function ensureDir(dir: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
function fsPath(filePath: string): string {
|
||||
if (process.platform !== 'win32') return filePath;
|
||||
if (!filePath) return filePath;
|
||||
if (filePath.startsWith('\\\\?\\')) return filePath;
|
||||
const windowsPath = filePath.replace(/\//g, '\\');
|
||||
if (!isAbsolute(windowsPath)) return windowsPath;
|
||||
if (windowsPath.startsWith('\\\\')) {
|
||||
return `\\\\?\\UNC\\${windowsPath.slice(2)}`;
|
||||
}
|
||||
return `\\\\?\\${windowsPath}`;
|
||||
}
|
||||
|
||||
function normalizeCandidatePath(dir: string): string {
|
||||
return normalize(expandPath(dir.trim()));
|
||||
}
|
||||
|
||||
function readOpenClawVersion(dir: string): string | undefined {
|
||||
try {
|
||||
const pkgPath = join(dir, 'package.json');
|
||||
if (existsSync(fsPath(pkgPath))) {
|
||||
const pkg = JSON.parse(readFileSync(fsPath(pkgPath), 'utf-8')) as { version?: string };
|
||||
return pkg.version;
|
||||
}
|
||||
} catch {
|
||||
// Ignore version read errors.
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isValidOpenClawPackageDir(dir: string): boolean {
|
||||
return Boolean(dir)
|
||||
&& existsSync(fsPath(dir))
|
||||
&& existsSync(fsPath(join(dir, 'package.json')))
|
||||
&& existsSync(fsPath(join(dir, 'openclaw.mjs')));
|
||||
}
|
||||
|
||||
function samePath(left: string, right: string): boolean {
|
||||
try {
|
||||
return realpathSync(fsPath(left)) === realpathSync(fsPath(right));
|
||||
} catch {
|
||||
return normalize(left) === normalize(right);
|
||||
}
|
||||
}
|
||||
|
||||
function logOpenClawRuntime(message: string, extra?: unknown): void {
|
||||
try {
|
||||
const { logger } = require('./logger') as typeof import('./logger');
|
||||
if (extra === undefined) {
|
||||
logger.info(message);
|
||||
} else {
|
||||
logger.info(message, extra);
|
||||
}
|
||||
} catch {
|
||||
// Logger may be unavailable in unit tests or non-Electron contexts.
|
||||
}
|
||||
}
|
||||
|
||||
function getBundledOpenClawDir(): string {
|
||||
if (getElectronApp().isPackaged) {
|
||||
return join(process.resourcesPath, 'openclaw');
|
||||
}
|
||||
return join(__dirname, '../../node_modules/openclaw');
|
||||
}
|
||||
|
||||
function getManagedOpenClawDir(): string {
|
||||
return join(getOpenClawConfigDir(), 'runtime', 'openclaw');
|
||||
}
|
||||
|
||||
function getEnvOpenClawCandidates(): string[] {
|
||||
return [
|
||||
process.env.YINIAN_OPENCLAW_DIR,
|
||||
process.env.CLAWX_OPENCLAW_DIR,
|
||||
process.env.OPENCLAW_DIR,
|
||||
].filter((value): value is string => Boolean(value?.trim()));
|
||||
}
|
||||
|
||||
function getStandardOpenClawCandidates(): string[] {
|
||||
const home = homedir();
|
||||
const candidates = [
|
||||
join(home, '.openclaw', 'openclaw'),
|
||||
join(home, '.openclaw', 'node_modules', 'openclaw'),
|
||||
join(home, '.npm-global', 'lib', 'node_modules', 'openclaw'),
|
||||
join(home, '.local', 'lib', 'node_modules', 'openclaw'),
|
||||
];
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
candidates.push(
|
||||
'/usr/local/lib/node_modules/openclaw',
|
||||
'/opt/homebrew/lib/node_modules/openclaw',
|
||||
);
|
||||
} else if (process.platform === 'linux') {
|
||||
candidates.push(
|
||||
'/usr/local/lib/node_modules/openclaw',
|
||||
'/usr/lib/node_modules/openclaw',
|
||||
);
|
||||
} else if (process.platform === 'win32') {
|
||||
const appData = process.env.APPDATA;
|
||||
const programFiles = process.env.ProgramFiles;
|
||||
const programFilesX86 = process.env['ProgramFiles(x86)'];
|
||||
if (appData) candidates.push(join(appData, 'npm', 'node_modules', 'openclaw'));
|
||||
if (programFiles) candidates.push(join(programFiles, 'nodejs', 'node_modules', 'openclaw'));
|
||||
if (programFilesX86) candidates.push(join(programFilesX86, 'nodejs', 'node_modules', 'openclaw'));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function findExternalOpenClawDir(excludedDirs: string[]): string | null {
|
||||
const seen = new Set<string>();
|
||||
const candidates = [...getEnvOpenClawCandidates(), ...getStandardOpenClawCandidates()];
|
||||
|
||||
for (const rawCandidate of candidates) {
|
||||
const candidate = normalizeCandidatePath(rawCandidate);
|
||||
if (seen.has(candidate)) continue;
|
||||
seen.add(candidate);
|
||||
if (excludedDirs.some((excluded) => samePath(candidate, excluded))) continue;
|
||||
if (isValidOpenClawPackageDir(candidate)) return candidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function installBundledOpenClawToManagedRuntime(bundledDir: string, managedDir: string): boolean {
|
||||
if (!isValidOpenClawPackageDir(bundledDir)) return false;
|
||||
|
||||
const bundledVersion = readOpenClawVersion(bundledDir);
|
||||
const managedVersion = isValidOpenClawPackageDir(managedDir)
|
||||
? readOpenClawVersion(managedDir)
|
||||
: undefined;
|
||||
|
||||
if (managedVersion && bundledVersion && managedVersion === bundledVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tempDir = `${managedDir}.tmp-${Date.now()}`;
|
||||
rmSync(fsPath(tempDir), { recursive: true, force: true });
|
||||
mkdirSync(fsPath(dirname(tempDir)), { recursive: true });
|
||||
cpSync(fsPath(bundledDir), fsPath(tempDir), { recursive: true, dereference: true });
|
||||
rmSync(fsPath(managedDir), { recursive: true, force: true });
|
||||
cpSync(fsPath(tempDir), fsPath(managedDir), { recursive: true, dereference: true });
|
||||
rmSync(fsPath(tempDir), { recursive: true, force: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveOpenClawRuntime(): OpenClawRuntimeResolution {
|
||||
if (cachedOpenClawRuntime) return cachedOpenClawRuntime;
|
||||
|
||||
const app = getElectronApp();
|
||||
const bundledDir = getBundledOpenClawDir();
|
||||
const managedDir = getManagedOpenClawDir();
|
||||
|
||||
if (!app.isPackaged) {
|
||||
cachedOpenClawRuntime = {
|
||||
dir: bundledDir,
|
||||
source: isValidOpenClawPackageDir(bundledDir) ? 'dev' : 'missing',
|
||||
version: readOpenClawVersion(bundledDir),
|
||||
bundledDir,
|
||||
managedDir,
|
||||
};
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
const externalDir = findExternalOpenClawDir([bundledDir, managedDir]);
|
||||
if (externalDir) {
|
||||
cachedOpenClawRuntime = {
|
||||
dir: externalDir,
|
||||
source: 'external',
|
||||
version: readOpenClawVersion(externalDir),
|
||||
bundledDir,
|
||||
managedDir,
|
||||
};
|
||||
logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation', cachedOpenClawRuntime);
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
let installedFromBundled = false;
|
||||
if (isValidOpenClawPackageDir(bundledDir)) {
|
||||
try {
|
||||
installedFromBundled = installBundledOpenClawToManagedRuntime(bundledDir, managedDir);
|
||||
} catch (error) {
|
||||
logOpenClawRuntime('[openclaw-runtime] Failed to install bundled OpenClaw runtime, falling back to bundled resources', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidOpenClawPackageDir(managedDir)) {
|
||||
cachedOpenClawRuntime = {
|
||||
dir: managedDir,
|
||||
source: 'managed',
|
||||
version: readOpenClawVersion(managedDir),
|
||||
bundledDir,
|
||||
managedDir,
|
||||
installedFromBundled,
|
||||
};
|
||||
logOpenClawRuntime(
|
||||
installedFromBundled
|
||||
? '[openclaw-runtime] Installed bundled OpenClaw runtime'
|
||||
: '[openclaw-runtime] Using managed OpenClaw runtime',
|
||||
cachedOpenClawRuntime,
|
||||
);
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
cachedOpenClawRuntime = {
|
||||
dir: bundledDir,
|
||||
source: isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing',
|
||||
version: readOpenClawVersion(bundledDir),
|
||||
bundledDir,
|
||||
managedDir,
|
||||
};
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resources directory (for bundled assets)
|
||||
*/
|
||||
@@ -109,16 +333,17 @@ export function getPreloadPath(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenClaw package directory
|
||||
* - Production (packaged): from resources/openclaw (copied by electron-builder extraResources)
|
||||
* - Development: from node_modules/openclaw
|
||||
* Get OpenClaw package directory.
|
||||
*
|
||||
* Runtime resolution policy:
|
||||
* 1. Development uses workspace node_modules/openclaw.
|
||||
* 2. Packaged builds first adapt an existing user/system OpenClaw package.
|
||||
* 3. If none exists, copy the bundled OpenClaw package into
|
||||
* ~/.openclaw/runtime/openclaw and run from there.
|
||||
* 4. If the managed install fails, fall back to packaged resources/openclaw.
|
||||
*/
|
||||
export function getOpenClawDir(): string {
|
||||
if (getElectronApp().isPackaged) {
|
||||
return join(process.resourcesPath, 'openclaw');
|
||||
}
|
||||
// Development: use node_modules/openclaw
|
||||
return join(__dirname, '../../node_modules/openclaw');
|
||||
return resolveOpenClawRuntime().dir;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,29 +413,26 @@ export interface OpenClawStatus {
|
||||
entryPath: string;
|
||||
dir: string;
|
||||
version?: string;
|
||||
source?: OpenClawRuntimeSource;
|
||||
bundledDir?: string;
|
||||
managedDir?: string;
|
||||
installedFromBundled?: boolean;
|
||||
}
|
||||
|
||||
export function getOpenClawStatus(): OpenClawStatus {
|
||||
const dir = getOpenClawDir();
|
||||
let version: string | undefined;
|
||||
|
||||
// Try to read version from package.json
|
||||
try {
|
||||
const pkgPath = join(dir, 'package.json');
|
||||
if (existsSync(pkgPath)) {
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||
version = pkg.version;
|
||||
}
|
||||
} catch {
|
||||
// Ignore version read errors
|
||||
}
|
||||
const runtime = resolveOpenClawRuntime();
|
||||
const dir = runtime.dir;
|
||||
|
||||
const status: OpenClawStatus = {
|
||||
packageExists: isOpenClawPresent(),
|
||||
isBuilt: isOpenClawBuilt(),
|
||||
entryPath: getOpenClawEntryPath(),
|
||||
dir,
|
||||
version,
|
||||
version: runtime.version,
|
||||
source: runtime.source,
|
||||
bundledDir: runtime.bundledDir,
|
||||
managedDir: runtime.managedDir,
|
||||
installedFromBundled: runtime.installedFromBundled,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -483,12 +483,15 @@ export function buildCandidateSources(pluginDirName: string): string[] {
|
||||
return app.isPackaged
|
||||
? [
|
||||
join(process.resourcesPath, 'openclaw-plugins', pluginDirName),
|
||||
join(process.resourcesPath, 'resources', 'openclaw-plugins', pluginDirName),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', pluginDirName),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', pluginDirName),
|
||||
]
|
||||
: [
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', pluginDirName),
|
||||
join(app.getAppPath(), 'resources', 'openclaw-plugins', pluginDirName),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', pluginDirName),
|
||||
join(process.cwd(), 'resources', 'openclaw-plugins', pluginDirName),
|
||||
join(__dirname, '../../build/openclaw-plugins', pluginDirName),
|
||||
];
|
||||
}
|
||||
@@ -517,6 +520,10 @@ export function ensureWeChatPluginInstalled(): { installed: boolean; warning?: s
|
||||
return ensurePluginInstalled('openclaw-weixin', buildCandidateSources('openclaw-weixin'), 'WeChat');
|
||||
}
|
||||
|
||||
export function ensureCloudSyncPluginInstalled(): { installed: boolean; warning?: string } {
|
||||
return ensurePluginInstalled('cloud-sync', buildCandidateSources('cloud-sync'), 'Cloud Sync');
|
||||
}
|
||||
|
||||
// ── Bulk startup installer ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -528,6 +535,7 @@ const ALL_BUNDLED_PLUGINS = [
|
||||
|
||||
{ fn: ensureFeishuPluginInstalled, label: 'Feishu' },
|
||||
{ fn: ensureWeChatPluginInstalled, label: 'WeChat' },
|
||||
{ fn: ensureCloudSyncPluginInstalled, label: 'Cloud Sync' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
|
||||
@@ -334,6 +334,27 @@ export async function saveWeChatAccountState(rawAccountId: string, payload: {
|
||||
return accountId;
|
||||
}
|
||||
|
||||
export async function listWeChatAccountAliases(): Promise<Array<{ accountId: string; userId: string }>> {
|
||||
const accountIds = await readAccountIndex();
|
||||
const aliases: Array<{ accountId: string; userId: string }> = [];
|
||||
|
||||
for (const accountId of accountIds) {
|
||||
try {
|
||||
const filePath = join(WECHAT_ACCOUNTS_DIR, `${normalizeOpenClawAccountId(accountId)}.json`);
|
||||
const raw = await readFile(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw) as { userId?: unknown };
|
||||
const userId = typeof parsed.userId === 'string' ? parsed.userId.trim() : '';
|
||||
if (userId) {
|
||||
aliases.push({ accountId: normalizeOpenClawAccountId(accountId), userId });
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed or removed account state files.
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
|
||||
export async function startWeChatLoginSession(options: {
|
||||
sessionKey?: string;
|
||||
accountId?: string;
|
||||
|
||||
Reference in New Issue
Block a user