Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ release
# Locally downloaded Lark CLI binary (dev/build) — produced by scripts/download-lark-cli.ts
.lark-cli-bin/

# Locally downloaded WeCom CLI binary (dev/build) — produced by scripts/download-wecom-cli.ts
.wecom-cli-bin/

# Bundled oo/opencode binaries staged by scripts/prepare-binaries.ts (not committed)
resources/bin

Expand All @@ -51,6 +54,9 @@ resources/skills
# Lark CLI skills exported from the pinned binary (not committed)
resources/lark-skills

# WeCom CLI skills exported from the pinned source commit (not committed)
resources/wecom-skills

# Bundled self-contained OpenCode custom-tool runtime (not committed)
resources/agent-tool-runtime

Expand Down
4 changes: 2 additions & 2 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ Wanta
Copyright 2026 OOMOL

This product includes software developed by third parties. In particular, Wanta uses OpenCode as
its local Agent engine and distributes the oo CLI and its bundled Skills for Connector access.
License and attribution details are provided in THIRD_PARTY_NOTICES.md.
its local Agent engine and distributes the oo CLI, the WeCom CLI, and their bundled Skills for
connected-app access. License and attribution details are provided in THIRD_PARTY_NOTICES.md.

Wanta and OOMOL names and logos are not licensed under the Apache License, Version 2.0. See
TRADEMARKS.md.
12 changes: 11 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,25 @@ The CLI and Skills are included by default so official OOMOL Connector and endpo
self-hosted OpenConnector deployments can use the same invocation path. Local BYOK mode does not
register Connector tools or inject the oo runtime environment.

## WeCom CLI and Skills

Wanta packages the official `@wecom/cli@0.1.9` platform binary and the matching `wecomcli-*`
Skills from source commit `72e14f7695f34d28f1ff23ea504ddd2210a87c13` for the local WeCom Direct
provider.

Source: [WecomTeam/wecom-cli](https://github.com/WecomTeam/wecom-cli). License: MIT. Copyright (c)
2026 WeCom.

## MIT License Text

The following text applies to the OpenCode and oo CLI entries above:
The following text applies to the OpenCode, oo CLI, and WeCom CLI entries above:

```text
MIT License

Copyright (c) 2025 opencode
Copyright (c) 2026 OOMOL Lab
Copyright (c) 2026 WeCom

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
16 changes: 15 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ config root is injected through `LARKSUITE_CLI_CONFIG_DIR`, and its matching Ski
the private Agent workspace. Thus chat uses the same local identity authorized from Connections,
independently of the selected OOMOL/OpenConnector Link runtime.

WeCom CLI is a separate local `direct` provider with a provider-specific QR-code experience rather
than a Lark-shaped authorization flow. `WecomCliManager` runs the official
`init --noninteractive --no-open` command against `<userData>/wecom-cli/config`, opens only the
allowlisted `https://work.weixin.qq.com/ai/qc/gen` page, keeps the short-lived `scode` URL in main
process memory, and lets the user reopen or cancel the scan while it is pending. Connection state is
read through the CLI's hidden `auth show` contract; only the bot ID, CLI version, phase, and redacted
errors cross `LinkRuntimeServiceImpl`. Disconnect removes only Wanta's isolated WeCom config and
temporary-media directories, never the user's global `~/.config/wecom` or the robot in WeCom. The
shipped platform binary is downloaded from the matching `@wecom/cli-*` npm package and verified
against registry `dist.integrity`; `wecomcli-*` Skills come from the exact source `gitHead` recorded
by the pinned `@wecom/cli` package. `WECOM_CLI_CONFIG_DIR` / `WECOM_CLI_TMP_DIR`, the managed binary
directory, and these Skills are injected into the private Agent runtime independently of Lark and of
the selected Link backend. Credentials and raw QR output never enter the renderer.

Vite (`vite-plugin-electron/simple` in `vite.config.ts`) bundles `electron/main.ts` and
`electron/preload.ts` into `dist-electron/main.js` + `preload.js`; the main-process build has a
**third** rollup input, `electron/chat/spreadsheet-preview-worker.ts` → `dist-electron/spreadsheet-preview-worker.js`,
Expand Down Expand Up @@ -621,7 +635,7 @@ electron/
chat/ common,node + ~45 modules by far the largest main-process domain: SSE event bridge; per-turn lifecycle & outputs (turn-lifecycle, turn-outputs); structured artifact registration/persistence (artifact-bundles, artifacts) + previews (spreadsheet-preview-worker[-client]); permission / local-access policy (permission-state, project-permission); project-* commands; attachments; stream buffering (stream-event-buffer, context-system). Also thin main-process facades openExternalUrl (shell external open) / setAgentTeam (agent team scope) for the renderer request layer (§4, §5)
git/ common,node,status,turn-diff(+test) GitService (serviceName("git-service")): project git status + per-turn diff review
knowledge/ common,node,store,runner,uri,thumbnail(+test) WikiGraph knowledge-base import, registration, query runtime & RPC service
link-runtime/ common,node,lark-cli(+test) selected Link runtime, origin-bound OpenConnector token, health/inventory facade, and isolated direct Lark CLI lifecycle
link-runtime/ common,node,lark-cli,wecom-cli(+test) selected Link runtime, origin-bound OpenConnector token, health/inventory facade, and isolated provider-specific Lark/WeCom direct CLI lifecycles
teams/ common types only, no node.ts — team requests moved renderer-side (src/lib/teams-client.ts, §4)
connections/ common,summary,usage,executions,federated,domain,summary-model(+test) **pure functions + types, no node.ts** — connector requests moved renderer-side (src/lib/connections-client.ts, §4/§7); electron-free, imported straight into the renderer bundle
skills/ common,node,actions,scan,inventory,… skill service (install/scan/inventory); browse GET moved renderer-side (src/lib/skills-catalog-client.ts); actions.ts normalize* reused by the renderer (§4)
Expand Down
4 changes: 4 additions & 0 deletions electron-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export default {
from: "resources/lark-skills",
to: "lark-skills",
},
{
from: "resources/wecom-skills",
to: "wecom-skills",
},
{
from: "resources/agent-tool-runtime",
to: "agent-tool-runtime",
Expand Down
16 changes: 16 additions & 0 deletions electron/agent/binaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export function larkCliBinaryName(platform: NodeJS.Platform = process.platform):
return platform === "win32" ? "lark-cli.exe" : "lark-cli"
}

export function wecomCliBinaryName(platform: NodeJS.Platform = process.platform): string {
return platform === "win32" ? "wecom-cli.exe" : "wecom-cli"
}

/** dev:从项目本地 .oo-bin 解析 oo 二进制(postinstall 下载、prepare-binaries 同源;生产由 extraResources 解析)。 */
export function resolveDevOoBin(repoRoot: string, platform: NodeJS.Platform = process.platform): string {
return path.join(repoRoot, ".oo-bin", ooBinaryName(platform))
Expand All @@ -35,6 +39,10 @@ export function resolveDevLarkCliBin(repoRoot: string, platform: NodeJS.Platform
return path.join(repoRoot, ".lark-cli-bin", larkCliBinaryName(platform))
}

export function resolveDevWecomCliBin(repoRoot: string, platform: NodeJS.Platform = process.platform): string {
return path.join(repoRoot, ".wecom-cli-bin", wecomCliBinaryName(platform))
}

/** 生产:从打包的 Resources/bin 解析二进制(prepare-binaries 复制、extraResources 打入)。 */
export function resolveBundledBin(resourcesPath: string, binaryName: string): string {
return path.join(resourcesPath, "bin", binaryName)
Expand All @@ -58,6 +66,14 @@ export function resolveBundledLarkSkillsDir(resourcesPath: string): string {
return path.join(resourcesPath, "lark-skills")
}

export function resolveDevBundledWecomSkillsDir(repoRoot: string): string {
return path.join(repoRoot, "resources", "wecom-skills")
}

export function resolveBundledWecomSkillsDir(resourcesPath: string): string {
return path.join(resourcesPath, "wecom-skills")
}

/** dev:构建期合并的自定义工具 runtime(postinstall 生成)。 */
export function resolveDevBundledToolRuntimePath(repoRoot: string): string {
return path.join(repoRoot, "resources", "agent-tool-runtime", "tool.js")
Expand Down
23 changes: 23 additions & 0 deletions electron/agent/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ describe("AgentManager", () => {
expect(env).not.toHaveProperty("WIKIGRAPH_STATE_DIR")
})

it("isolates local direct CLI configuration in the sidecar", () => {
const env = buildAgentSidecarEnv({
commandPath: "/managed/bin:/usr/bin",
larkCliBinPath: "/managed/bin/lark-cli",
larkCliConfigDir: "/private/lark/config",
linkRuntime: null,
storeDir: "/private/oo-store",
teamScopePath: "/private/team-scope.json",
wecomCliBinPath: "/managed/bin/wecom-cli",
wecomCliConfigDir: "/private/wecom/config",
wecomCliTmpDir: "/private/wecom/tmp",
})

expect(env).toMatchObject({
LARKSUITE_CLI_CONFIG_DIR: "/private/lark/config",
PATH: "/managed/bin:/usr/bin",
WANTA_LARK_CLI_BIN: "/managed/bin/lark-cli",
WANTA_WECOM_CLI_BIN: "/managed/bin/wecom-cli",
WECOM_CLI_CONFIG_DIR: "/private/wecom/config",
WECOM_CLI_TMP_DIR: "/private/wecom/tmp",
})
})

it("exposes OOMOL authentication and the managed Node runtime to Skill commands", () => {
const env = buildAgentSidecarEnv({
commandPath: "/usr/bin:/bin",
Expand Down
27 changes: 24 additions & 3 deletions electron/agent/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@ export interface AgentManagerOptions {
listOpenConnectorAuthorizedServices?: (signal?: AbortSignal) => Promise<string[]>
/** 内置 skill 源目录(resources/skills 或打包 Resources/skills);启动时拷进 .opencode/skill/。 */
bundledSkillsDir?: string
/** Official Lark CLI skills, available for the local direct connection. */
bundledLarkSkillsDir?: string
/** Official local direct-CLI skills, independent of the selected Link runtime. */
bundledDirectSkillsDirs?: string[]
/** Active Wanta-managed Lark CLI direct-runtime binary. */
larkCliBinPath?: string
/** Isolated Lark CLI config directory; credentials remain owned by the CLI/keychain. */
larkCliConfigDir?: string
/** Active Wanta-managed WeCom CLI direct-runtime binary. */
wecomCliBinPath?: string
/** Isolated WeCom CLI config and temporary roots. */
wecomCliConfigDir?: string
wecomCliTmpDir?: string
/** 构建期合并的自定义工具 runtime;启动时拷进 .opencode/runtime/tool.js。 */
bundledToolRuntimePath?: string
/** App 私有根目录(userData 下):workspace / oo-store / isolation 都在其下。 */
Expand Down Expand Up @@ -114,6 +119,9 @@ export interface AgentSidecarEnvOptions {
teamScopePath: string
larkCliBinPath?: string
larkCliConfigDir?: string
wecomCliBinPath?: string
wecomCliConfigDir?: string
wecomCliTmpDir?: string
}

export function buildAgentSidecarEnv({
Expand All @@ -126,6 +134,9 @@ export function buildAgentSidecarEnv({
teamScopePath,
larkCliBinPath,
larkCliConfigDir,
wecomCliBinPath,
wecomCliConfigDir,
wecomCliTmpDir,
}: AgentSidecarEnvOptions): Record<string, string> {
const ooEnv = linkRuntime
? buildAgentLinkEnv({
Expand All @@ -146,6 +157,9 @@ export function buildAgentSidecarEnv({
LARKSUITE_CLI_CONFIG_DIR: larkCliConfigDir ?? "",
LARKSUITE_CLI_NO_SKILLS_NOTIFIER: "1",
LARKSUITE_CLI_NO_UPDATE_NOTIFIER: "1",
WANTA_WECOM_CLI_BIN: wecomCliBinPath ?? "",
WECOM_CLI_CONFIG_DIR: wecomCliConfigDir ?? "",
WECOM_CLI_TMP_DIR: wecomCliTmpDir ?? "",
}
}

Expand Down Expand Up @@ -429,8 +443,8 @@ export class AgentManager {

await ensureAgentWorkspace(workspaceDir, bundledSkillsDir, bundledToolRuntimePath, {
bundledOoSkills: this.options.linkRuntime?.kind === "oomol",
bundledLarkSkillsDir: this.options.bundledLarkSkillsDir,
connectors: this.options.linkRuntime !== null,
directSkillsDirs: this.options.bundledDirectSkillsDirs,
})
this.teamScopePath = teamScopePath
await this.writeTeamState(this.teamName)
Expand All @@ -450,6 +464,9 @@ export class AgentManager {
wikiGraphStateDir,
larkCliBinPath,
larkCliConfigDir,
wecomCliBinPath,
wecomCliConfigDir,
wecomCliTmpDir,
} = this.options
const workspaceDir = path.join(rootDir, "workspace")
const isolationDir = path.join(rootDir, "isolation")
Expand All @@ -460,6 +477,7 @@ export class AgentManager {
const baseCommandPath = await resolveUserCommandPath({
preferredDirectories: [
...(larkCliBinPath ? [path.dirname(larkCliBinPath)] : []),
...(wecomCliBinPath ? [path.dirname(wecomCliBinPath)] : []),
...(linkRuntime && ooBinPath ? [path.dirname(ooBinPath)] : []),
],
})
Expand All @@ -484,6 +502,9 @@ export class AgentManager {
teamScopePath,
larkCliBinPath,
larkCliConfigDir,
wecomCliBinPath,
wecomCliConfigDir,
wecomCliTmpDir,
})

const sidecar = new OpencodeSidecar({
Expand Down
4 changes: 4 additions & 0 deletions electron/agent/sidecar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,10 @@ export class OpencodeSidecar {
XDG_CONFIG_HOME: xdgConfigHome,
XDG_DATA_HOME: xdgDataHome,
}
// WeCom commands inherit this environment from OpenCode. Never let a launcher-level logging
// override redirect credential-adjacent CLI diagnostics outside Wanta's private runtime.
delete baseChildEnv.WECOM_CLI_LOG_FILE
delete baseChildEnv.WECOM_CLI_LOG_LEVEL
const proxy = await systemProxy()
const childEnv = mergeSystemProxyEnvironment(baseChildEnv, proxy)
logDiagnostic("opencode-sidecar", "opencode sidecar network environment", {
Expand Down
26 changes: 25 additions & 1 deletion electron/agent/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ test("ensureAgentWorkspace installs Lark direct-mode skills independently of the
await writeSkill(bundledLarkSkillsDir, "lark-calendar")

await ensureAgentWorkspace(workspaceDir, bundledSkillsDir, bundledToolRuntimePath, {
bundledLarkSkillsDir,
directSkillsDirs: [bundledLarkSkillsDir],
bundledOoSkills: false,
connectors: false,
})
Expand All @@ -185,6 +185,30 @@ test("ensureAgentWorkspace installs Lark direct-mode skills independently of the
}
})

test("ensureAgentWorkspace installs independent Lark and WeCom direct-mode skills", async () => {
const base = await mkdtemp(path.join(os.tmpdir(), "wanta-workspace-"))
try {
const workspaceDir = path.join(base, "workspace")
const bundledToolRuntimePath = await writeToolRuntime(base)
const larkSkillsDir = path.join(base, "lark-skills")
const wecomSkillsDir = path.join(base, "wecom-skills")
await writeSkill(larkSkillsDir, "lark-calendar")
await writeSkill(wecomSkillsDir, "wecomcli-todo")

await ensureAgentWorkspace(workspaceDir, undefined, bundledToolRuntimePath, {
bundledOoSkills: false,
connectors: false,
directSkillsDirs: [larkSkillsDir, wecomSkillsDir],
})

const skillRoot = path.join(workspaceDir, ".opencode", "skill")
assert.ok(await exists(path.join(skillRoot, "lark-calendar", "SKILL.md")))
assert.ok(await exists(path.join(skillRoot, "wecomcli-todo", "SKILL.md")))
} finally {
await rm(base, { force: true, recursive: true })
}
})

test("ensureAgentWorkspace works without a bundled skills directory", async () => {
const base = await mkdtemp(path.join(os.tmpdir(), "wanta-workspace-"))
try {
Expand Down
18 changes: 8 additions & 10 deletions electron/agent/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const alwaysAvailableBundledSkillIds = new Set(["browser"])

export interface AgentWorkspaceOptions {
bundledOoSkills: boolean
bundledLarkSkillsDir?: string
directSkillsDirs?: string[]
connectors: boolean
}

Expand Down Expand Up @@ -37,7 +37,7 @@ export async function ensureAgentWorkspace(
),
)
await syncToolRuntime(opencodeDir, bundledToolRuntimePath)
await syncBundledSkills(opencodeDir, bundledSkillsDir, options.bundledLarkSkillsDir, options.bundledOoSkills)
await syncBundledSkills(opencodeDir, bundledSkillsDir, options.directSkillsDirs ?? [], options.bundledOoSkills)
return rootDir
}

Expand All @@ -63,22 +63,23 @@ async function syncToolRuntime(opencodeDir: string, bundledToolRuntimePath: stri
async function syncBundledSkills(
opencodeDir: string,
bundledSkillsDir: string | undefined,
bundledLarkSkillsDir: string | undefined,
directSkillsDirs: string[],
includeOomolSkills: boolean,
): Promise<void> {
const skillDir = path.join(opencodeDir, "skill")

if (!bundledSkillsDir && !bundledLarkSkillsDir) {
if (!bundledSkillsDir && directSkillsDirs.length === 0) {
await rm(skillDir, { force: true, recursive: true })
return
}

const sources: Array<{ directory: string; names: string[] }> = []
for (const directory of [bundledSkillsDir, bundledLarkSkillsDir]) {
const sources: Array<{ alwaysInclude: boolean; directory: string; names: string[] }> = []
for (const directory of [bundledSkillsDir, ...directSkillsDirs]) {
if (!directory) continue
try {
const entries = await readdir(directory, { withFileTypes: true })
sources.push({
alwaysInclude: directSkillsDirs.includes(directory),
directory,
names: entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name),
})
Expand All @@ -95,10 +96,7 @@ async function syncBundledSkills(

const skillSources = sources.flatMap((source) =>
source.names
.filter(
(name) =>
source.directory === bundledLarkSkillsDir || includeOomolSkills || alwaysAvailableBundledSkillIds.has(name),
)
.filter((name) => source.alwaysInclude || includeOomolSkills || alwaysAvailableBundledSkillIds.has(name))
.map((name) => ({ name, source: source.directory })),
)
if (skillSources.length === 0) {
Expand Down
6 changes: 6 additions & 0 deletions electron/connections/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ export interface ConnectionProviderSummary {
authTypes: Exclude<ConnectionAuthType, null>[]
actionKind: ConnectionProviderActionKind
canDisconnect: boolean
/** Local Direct providers opt into reconnect only when their runtime can replace an existing identity. */
canReconnect?: boolean
categoryLabels: string[]
/** Renderer-owned copy for a local Direct provider's provider-specific primary action. */
connectActionLabel?: string
/** Renderer-owned copy for a local Direct provider's non-OAuth connection method. */
connectionMethodLabel?: string
connectedUpdatedAt?: number
displayName: string
description?: string
Expand Down
Loading
Loading