From ec2803e7b302be37a98f9abe9f195073d06335b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=BF=8A=E9=BE=99?= Date: Wed, 9 Sep 2026 18:09:01 +0800 Subject: [PATCH 1/4] feat(lark): connect local login with managed cloud authentication --- .github/workflows/lark-package.yml | 68 +++++ plugins/lark/.codex-plugin/plugin.json | 67 +++- plugins/lark/.wework-build.json | 7 + plugins/lark/.wework-build/lark-auth/build.py | 134 ++++++++ .../lark-auth/overlay/business.go | 145 +++++++++ .../.wework-build/lark-auth/overlay/main.go | 56 ++++ .../lark-auth/overlay/provider.go | 265 ++++++++++++++++ .../lark-auth/overlay/provider_test.go | 289 ++++++++++++++++++ .../.wework-build/lark-auth/overlay/source.go | 160 ++++++++++ .../lark/.wework-build/lark-auth/package.py | 103 +++++++ .../lark/.wework-build/lark-auth/toolchain.py | 86 ++++++ .../lark/.wework-build/lark-auth/verify.py | 111 +++++++ .../lark/.wework-build/plugin-auth-go/LICENSE | 73 +++++ .../.wework-build/plugin-auth-go/README.en.md | 45 +++ .../.wework-build/plugin-auth-go/README.md | 38 +++ .../.wework-build/plugin-auth-go/adapter.go | 155 ++++++++++ .../plugin-auth-go/configuration.go | 38 +++ .../plugin-auth-go/configuration_test.go | 23 ++ .../lark/.wework-build/plugin-auth-go/go.mod | 3 + .../.wework-build/plugin-auth-go/transport.go | 181 +++++++++++ .../plugin-auth-go/transport_test.go | 175 +++++++++++ plugins/lark/README.md | 72 ++--- plugins/lark/scripts/account-auth-bot.py | 11 + plugins/lark/scripts/account-auth.py | 11 + plugins/lark/scripts/ensure-lark-ready.ps1 | 62 +--- plugins/lark/scripts/ensure-lark-ready.sh | 34 +-- plugins/lark/scripts/lark_cli.py | 113 +++++++ plugins/lark/scripts/local-auth.ps1 | 9 + plugins/lark/scripts/local-auth.sh | 4 + plugins/lark/scripts/local_auth.py | 142 +++++++++ plugins/lark/scripts/native_runtime.py | 210 +++++++++++++ plugins/lark/scripts/run-lark-cli.ps1 | 31 +- plugins/lark/scripts/run-lark-cli.sh | 11 +- plugins/lark/scripts/run-python.ps1 | 16 + plugins/lark/scripts/run-python.sh | 10 + plugins/lark/scripts/tests/test_runtime.py | 191 ++++++++++++ .../lark/scripts/wegent_plugin_auth/LICENSE | 73 +++++ .../scripts/wegent_plugin_auth/__init__.py | 16 + .../scripts/wegent_plugin_auth/adapter.py | 195 ++++++++++++ .../wegent_plugin_auth/configuration.py | 37 +++ .../scripts/wegent_plugin_auth/runtime.py | 184 +++++++++++ .../scripts/wegent_plugin_auth/transport.py | 106 +++++++ .../scripts/wegent_plugin_auth/vendor.json | 13 + plugins/lark/skills/lark-approval/SKILL.md | 4 +- plugins/lark/skills/lark-attendance/SKILL.md | 4 +- plugins/lark/skills/lark-base/SKILL.md | 4 +- plugins/lark/skills/lark-calendar/SKILL.md | 4 +- plugins/lark/skills/lark-contact/SKILL.md | 4 +- plugins/lark/skills/lark-doc/SKILL.md | 4 +- plugins/lark/skills/lark-drive/SKILL.md | 4 +- plugins/lark/skills/lark-event/SKILL.md | 4 +- plugins/lark/skills/lark-im/SKILL.md | 4 +- plugins/lark/skills/lark-mail/SKILL.md | 4 +- plugins/lark/skills/lark-markdown/SKILL.md | 4 +- plugins/lark/skills/lark-minutes/SKILL.md | 4 +- plugins/lark/skills/lark-note/SKILL.md | 4 +- plugins/lark/skills/lark-okr/SKILL.md | 4 +- .../skills/lark-openapi-explorer/SKILL.md | 4 +- plugins/lark/skills/lark-shared/SKILL.md | 89 +----- plugins/lark/skills/lark-sheets/SKILL.md | 4 +- plugins/lark/skills/lark-skill-maker/SKILL.md | 4 +- plugins/lark/skills/lark-slides/SKILL.md | 4 +- plugins/lark/skills/lark-task/SKILL.md | 4 +- plugins/lark/skills/lark-vc-agent/SKILL.md | 4 +- plugins/lark/skills/lark-vc/SKILL.md | 4 +- plugins/lark/skills/lark-whiteboard/SKILL.md | 4 +- plugins/lark/skills/lark-wiki/SKILL.md | 4 +- .../lark-workflow-meeting-summary/SKILL.md | 4 +- .../lark-workflow-standup-report/SKILL.md | 4 +- 69 files changed, 3669 insertions(+), 293 deletions(-) create mode 100644 .github/workflows/lark-package.yml create mode 100644 plugins/lark/.wework-build.json create mode 100644 plugins/lark/.wework-build/lark-auth/build.py create mode 100644 plugins/lark/.wework-build/lark-auth/overlay/business.go create mode 100644 plugins/lark/.wework-build/lark-auth/overlay/main.go create mode 100644 plugins/lark/.wework-build/lark-auth/overlay/provider.go create mode 100644 plugins/lark/.wework-build/lark-auth/overlay/provider_test.go create mode 100644 plugins/lark/.wework-build/lark-auth/overlay/source.go create mode 100644 plugins/lark/.wework-build/lark-auth/package.py create mode 100644 plugins/lark/.wework-build/lark-auth/toolchain.py create mode 100644 plugins/lark/.wework-build/lark-auth/verify.py create mode 100644 plugins/lark/.wework-build/plugin-auth-go/LICENSE create mode 100644 plugins/lark/.wework-build/plugin-auth-go/README.en.md create mode 100644 plugins/lark/.wework-build/plugin-auth-go/README.md create mode 100644 plugins/lark/.wework-build/plugin-auth-go/adapter.go create mode 100644 plugins/lark/.wework-build/plugin-auth-go/configuration.go create mode 100644 plugins/lark/.wework-build/plugin-auth-go/configuration_test.go create mode 100644 plugins/lark/.wework-build/plugin-auth-go/go.mod create mode 100644 plugins/lark/.wework-build/plugin-auth-go/transport.go create mode 100644 plugins/lark/.wework-build/plugin-auth-go/transport_test.go create mode 100644 plugins/lark/scripts/account-auth-bot.py create mode 100644 plugins/lark/scripts/account-auth.py create mode 100644 plugins/lark/scripts/lark_cli.py create mode 100644 plugins/lark/scripts/local-auth.ps1 create mode 100644 plugins/lark/scripts/local-auth.sh create mode 100644 plugins/lark/scripts/local_auth.py create mode 100644 plugins/lark/scripts/native_runtime.py create mode 100644 plugins/lark/scripts/run-python.ps1 create mode 100644 plugins/lark/scripts/run-python.sh create mode 100644 plugins/lark/scripts/tests/test_runtime.py create mode 100644 plugins/lark/scripts/wegent_plugin_auth/LICENSE create mode 100644 plugins/lark/scripts/wegent_plugin_auth/__init__.py create mode 100644 plugins/lark/scripts/wegent_plugin_auth/adapter.py create mode 100644 plugins/lark/scripts/wegent_plugin_auth/configuration.py create mode 100644 plugins/lark/scripts/wegent_plugin_auth/runtime.py create mode 100644 plugins/lark/scripts/wegent_plugin_auth/transport.py create mode 100644 plugins/lark/scripts/wegent_plugin_auth/vendor.json diff --git a/.github/workflows/lark-package.yml b/.github/workflows/lark-package.yml new file mode 100644 index 0000000..1756c74 --- /dev/null +++ b/.github/workflows/lark-package.yml @@ -0,0 +1,68 @@ +name: Lark account authentication package + +on: + pull_request: + paths: + - 'plugins/lark/**' + - '.github/workflows/lark-package.yml' + push: + branches: [main] + paths: + - 'plugins/lark/**' + - '.github/workflows/lark-package.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + package: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v6 + with: + python-version: '3.12' + - uses: actions/setup-go@v6 + with: + go-version: '1.25.9' + cache: false + - name: Test routing and lifecycle + run: uv run --no-project python -m unittest discover -s plugins/lark/scripts/tests -v + - name: Build all targets and verify the packaged native entry + run: uv run --no-project python plugins/lark/.wework-build/lark-auth/package.py --plugin plugins/lark --output .ci-artifacts/lark-account-auth.zip + - name: Retain the tested candidate + if: runner.os == 'Linux' + uses: actions/upload-artifact@v4 + with: + name: lark-candidate-${{ github.sha }} + path: | + .ci-artifacts/lark-account-auth.zip + .ci-artifacts/lark-account-auth.zip.sha256 + if-no-files-found: error + retention-days: 7 + + release-artifact: + needs: package + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: lark-candidate-${{ github.sha }} + path: release + - name: Verify selected candidate + working-directory: release + run: sha256sum --check lark-account-auth.zip.sha256 + - uses: actions/upload-artifact@v4 + with: + name: lark-account-auth-${{ github.sha }} + path: release/ + if-no-files-found: error + retention-days: 14 diff --git a/plugins/lark/.codex-plugin/plugin.json b/plugins/lark/.codex-plugin/plugin.json index ec49950..6182eb5 100644 --- a/plugins/lark/.codex-plugin/plugin.json +++ b/plugins/lark/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "lark", - "version": "0.1.4", - "description": "通过本机已完成认证的官方 CLI,使用飞书/Lark 消息、文档、多维表格、电子表格、日历、任务、会议与企业协作能力。", + "version": "0.2.0", + "description": "通过飞书官方 CLI 使用协作能力,本机登录后由 Wegent 托管用户 OAuth 与应用机器人认证。", "author": { "name": "Wegent" }, @@ -23,8 +23,8 @@ "skills": "./skills/", "interface": { "displayName": "飞书", - "shortDescription": "通过本机官方 CLI 使用飞书完整协作能力", - "longDescription": "自动准备跨平台飞书官方 CLI,通过浏览器扫码在本机创建应用并完成用户 OAuth,支持消息、通讯录、文档、云盘、知识库、多维表格、电子表格、幻灯片、日历、任务、审批、邮箱、会议、妙记、OKR、考勤和实时事件。App Secret 与用户 Token 不上传 Wegent Backend。", + "shortDescription": "本地与云端复用飞书用户及应用认证", + "longDescription": "沿用飞书应用创建和浏览器用户授权,用户 OAuth 独占交接给 Wegent 统一刷新,应用机器人凭据单独托管。业务调用使用原生内存认证提供方,保留官方 CLI 的消息、文档、日历、表格等命令与确认规则。", "developerName": "Wegent", "category": "协作", "capabilities": [ @@ -43,5 +43,62 @@ "帮我整理飞书云空间和知识库中的资料。", "查看我的飞书日程和未完成任务并生成摘要。" ] - } + }, + "connectors": [ + { + "slug": "lark", + "authPolicy": "on_install", + "localAuth": { + "kind": "browser_oauth", + "health": [ + "scripts/local-auth.sh", + "health" + ], + "start": [ + "scripts/local-auth.sh", + "login" + ], + "logout": [ + "scripts/local-auth.sh", + "logout" + ], + "timeoutSeconds": 600, + "logoutOnUninstall": false + }, + "accountAuth": { + "protocolVersion": 1, + "credentialType": "oauth2", + "adapter": "scripts/account-auth.py", + "oauth2": [ + "refresh", + "revoke" + ], + "exportMode": "exclusive", + "localEnvironment": { + "LARKSUITE_CLI_CONFIG_DIR": { + "type": "directory" + }, + "XDG_DATA_HOME": { + "type": "directory" + } + } + } + }, + { + "slug": "lark-app", + "accountAuth": { + "protocolVersion": 1, + "credentialType": "password", + "adapter": "scripts/account-auth-bot.py", + "localEnvironment": { + "LARKSUITE_CLI_CONFIG_DIR": { + "type": "directory" + }, + "XDG_DATA_HOME": { + "type": "directory" + } + } + } + } + ] } diff --git a/plugins/lark/.wework-build.json b/plugins/lark/.wework-build.json new file mode 100644 index 0000000..59cbf2a --- /dev/null +++ b/plugins/lark/.wework-build.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 1, + "entrypoint": ".wework-build/lark-auth/package.py", + "outputs": [ + "scripts/native" + ] +} diff --git a/plugins/lark/.wework-build/lark-auth/build.py b/plugins/lark/.wework-build/lark-auth/build.py new file mode 100644 index 0000000..8d00292 --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/build.py @@ -0,0 +1,134 @@ +"""Build a pinned upstream CLI plus Wegent's private credential boundary.""" + +import argparse +import hashlib +import json +import lzma +import os +import shutil +import subprocess +import tarfile +import tempfile +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +VERSION = "1.0.68" +SOURCE_SHA256 = "e23a0f85116dc4ef869ccb4c124dc02e2847aa8c6f414ee7d736c39a070e3a95" + + +def source_archive(directory, provided=None): + path = provided or directory / "source.tar.gz" + if provided is None: + with urllib.request.urlopen( + f"https://codeload.github.com/larksuite/cli/tar.gz/refs/tags/v{VERSION}", + timeout=60, + ) as response: + path.write_bytes(response.read(100 * 1024 * 1024 + 1)) + if hashlib.sha256(path.read_bytes()).hexdigest() != SOURCE_SHA256: + raise ValueError("Lark source checksum mismatch") + return path + + +def prepare(directory, archive): + with tarfile.open(archive) as source: + source.extractall(directory, filter="data") + root = directory / ("cli-" + VERSION) + shutil.copytree(ROOT / "overlay", root / "cmd/wegent-account-auth") + sdk = root / "internal/wegentpluginauth" + sdk.mkdir() + for path in (ROOT.parent / "plugin-auth-go").glob("*.go"): + shutil.copyfile(path, sdk / path.name) + # Intercept every upstream keychain entry point in managed business mode, + # including direct UAT helpers that bypass Factory.WithKeychain. + path = root / "internal/keychain/keychain.go" + value = path.read_text() + changes = { + "func Get(service, account string) (string, error) {": "func Get(service, account string) (string, error) {\n if WegentAccess != nil { return WegentAccess.Get(service, account) }", + "func Set(service, account, data string) error {": "func Set(service, account, data string) error {\n if WegentAccess != nil { return WegentAccess.Set(service, account, data) }", + "func Remove(service, account string) error {": "func Remove(service, account string) error {\n if WegentAccess != nil { return WegentAccess.Remove(service, account) }", + } + for old, new in changes.items(): + if value.count(old) != 1: + raise ValueError("Upstream keychain boundary changed") + value = value.replace(old, new) + path.write_text( + value + + "\n// WegentAccess is set only in the native managed process.\nvar WegentAccess KeychainAccess\n" + ) + return root + + +def build(root, output, target): + system, arch = target.split("/") + environment = {**os.environ, "GOOS": system, "GOARCH": arch, "CGO_ENABLED": "0"} + raw = output.with_suffix(".exe" if system == "windows" else ".bin") + output.parent.mkdir(parents=True, exist_ok=True) + subprocess.run( + [ + "go", + "build", + "-trimpath", + "-ldflags=-s -w", + "-o", + str(raw), + "./cmd/wegent-account-auth", + ], + cwd=root, + env=environment, + check=True, + ) + content = raw.read_bytes() + output.write_bytes(lzma.compress(content, preset=9)) + metadata = { + "nativeProtocolVersion": 1, + "target": target, + "upstreamVersion": VERSION, + "upstreamSourceSha256": SOURCE_SHA256, + "binarySha256": hashlib.sha256(content).hexdigest(), + "binaryBytes": len(content), + "compressedSha256": hashlib.sha256(output.read_bytes()).hexdigest(), + "sources": { + p.relative_to(ROOT.parent) + .as_posix(): hashlib.sha256(p.read_bytes()) + .hexdigest() + for p in sorted( + [ + *ROOT.glob("*.py"), + *(ROOT / "overlay").glob("*.go"), + *(ROOT.parent / "plugin-auth-go").glob("*.go"), + ] + ) + }, + } + output.with_suffix(".json").write_text(json.dumps(metadata, indent=2) + "\n") + raw.unlink() + return metadata + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--source-archive", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--test", action="store_true") + args = parser.parse_args() + with tempfile.TemporaryDirectory(prefix="wegent-lark-build-") as temporary: + directory = Path(temporary) + root = prepare(directory, source_archive(directory, args.source_archive)) + if args.test: + subprocess.run( + [ + "go", + "test", + "./internal/wegentpluginauth", + "./cmd/wegent-account-auth", + ], + cwd=root, + check=True, + ) + print(json.dumps(build(root, args.output.resolve(), args.target))) + + +if __name__ == "__main__": + main() diff --git a/plugins/lark/.wework-build/lark-auth/overlay/business.go b/plugins/lark/.wework-build/lark-auth/overlay/business.go new file mode 100644 index 0000000..79d8efa --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/overlay/business.go @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT +package main + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "os" + "strings" + + "github.com/larksuite/cli/cmd" + ext "github.com/larksuite/cli/extension/credential" + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/core" + "github.com/larksuite/cli/internal/keychain" + pluginauth "github.com/larksuite/cli/internal/wegentpluginauth" +) + +var products = map[string]bool{"approval": true, "attendance": true, "base": true, "calendar": true, "contact": true, "docs": true, "doc": true, "drive": true, "event": true, "im": true, "mail": true, "md": true, "markdown": true, "mindnotes": true, "note": true, "minutes": true, "okr": true, "sheets": true, "slides": true, "task": true, "vc": true, "whiteboard": true, "wiki": true, "api": true, "schema": true, "whoami": true, "account-status": true} + +func allowed(args []string) bool { + if len(args) == 0 || !products[args[0]] { + return false + } + for _, arg := range args { + name := strings.SplitN(arg, "=", 2)[0] + switch name { + case "--profile", "--workspace", "--app-id", "--app-secret", "--token", "--access-token", "--base-url", "--brand", "--debug", "--verbose", "--trace", "--config", "--endpoint", "--plugin": + return false + } + } + return true +} + +type noKeychain struct{} + +func (noKeychain) Get(string, string) (string, error) { return "", denied } +func (noKeychain) Set(string, string, string) error { return denied } +func (noKeychain) Remove(string, string) error { return denied } + +type memoryProvider struct { + credential pluginauth.Credential + token string + bot bool +} + +func (p *memoryProvider) Name() string { return "wegent" } +func (p *memoryProvider) Priority() int { return -1000 } +func (p *memoryProvider) ResolveAccount(context.Context) (*ext.Account, error) { + identity, support := ext.IdentityUser, ext.SupportsUser + if p.bot { + identity, support = ext.IdentityBot, ext.SupportsBot + } + return &ext.Account{AppID: text(p.credential, "app_id"), Brand: ext.Brand(text(p.credential, "brand")), OpenID: text(p.credential, "open_id"), DefaultAs: identity, SupportedIdentities: support}, nil +} +func (p *memoryProvider) ResolveToken(_ context.Context, spec ext.TokenSpec) (*ext.Token, error) { + expected := ext.TokenTypeUAT + if p.bot { + expected = ext.TokenTypeTAT + } + if spec.Type != expected || (spec.AppID != "" && spec.AppID != text(p.credential, "app_id")) { + return nil, &ext.BlockError{Provider: "wegent", Reason: "identity not granted"} + } + return &ext.Token{Value: p.token, Scopes: text(p.credential, "scope"), Source: "wegent"}, nil +} + +func credentialOrigin(openHost string, req *http.Request) (*url.URL, error) { + // Upstream downloads may use CDN hosts, but no credential may follow them. + if req.Header.Get("Authorization") != "" && (req.URL.Scheme != "https" || req.URL.Host != openHost || (req.Host != "" && req.Host != openHost)) { + return nil, denied + } + return nil, nil +} + +type limitedBuffer struct{ bytes.Buffer } + +var newBusinessTransport = func(host string) *http.Transport { + return &http.Transport{Proxy: func(req *http.Request) (*url.URL, error) { return credentialOrigin(host, req) }} +} + +func (b *limitedBuffer) Write(p []byte) (int, error) { + if b.Len()+len(p) > 8*1024*1024 { + return 0, denied + } + return b.Buffer.Write(p) +} +func (b *limitedBuffer) WriteString(value string) (int, error) { return b.Write([]byte(value)) } +func runUser(ctx context.Context, c pluginauth.Credential, args []string) error { + if validate(c, false) != nil || c["refresh_token"] != nil || c["provider_private"] != nil { + return denied + } + return runCLI(ctx, c, args, text(c, "access_token"), false) +} +func runBot(ctx context.Context, c pluginauth.Credential, args []string) error { + token, err := botToken(ctx, c) + if err != nil { + return err + } + return runCLI(ctx, c, args, token, true) +} +func runCLI(ctx context.Context, c pluginauth.Credential, args []string, token string, bot bool) error { + if !allowed(args) { + return denied + } + if len(args) == 1 && args[0] == "account-status" { + if !bot && userInfo(ctx, c) != nil { + return denied + } + return json.NewEncoder(os.Stdout).Encode(map[string]any{"status": "ok", "accountId": accountID(c, bot)}) + } + directory, err := os.MkdirTemp("", "wegent-lark-runtime-") + if err != nil { + return denied + } + defer os.RemoveAll(directory) + // The upstream command tree gets an empty config location and a denying + // keychain. User grants are provided only by the in-memory provider. + os.Setenv("LARKSUITE_CLI_CONFIG_DIR", directory) + core.SetCurrentWorkspace(core.WorkspaceLocal) + keychain.WegentAccess = noKeychain{} + defer func() { keychain.WegentAccess = nil }() + ext.Register(&memoryProvider{c, token, bot}) + b, _ := brand(c) + origin, _ := url.Parse(core.ResolveEndpoints(b).Open) + oldTransport := http.DefaultTransport + // Keep a concrete Transport so upstream clones preserve this guard. + http.DefaultTransport = newBusinessTransport(origin.Host) + defer func() { http.DefaultTransport = oldTransport }() + var out, errOut limitedBuffer + root := cmd.Build(ctx, cmdutil.InvocationContext{}, cmd.WithIO(strings.NewReader(""), &out, &errOut), cmd.WithKeychain(noKeychain{}), cmd.WithoutPlugins(), cmd.HideProfile(true)) + root.SetArgs(args) + if root.ExecuteContext(ctx) != nil { + return denied + } + for _, secret := range []string{token, text(c, "password")} { + if secret != "" && (bytes.Contains(out.Bytes(), []byte(secret)) || bytes.Contains(errOut.Bytes(), []byte(secret))) { + return denied + } + } + _, err = io.Copy(os.Stdout, &out) + return err +} diff --git a/plugins/lark/.wework-build/lark-auth/overlay/main.go b/plugins/lark/.wework-build/lark-auth/overlay/main.go new file mode 100644 index 0000000..0a6072b --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/overlay/main.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/larksuite/cli/cmd" + pluginauth "github.com/larksuite/cli/internal/wegentpluginauth" +) + +func main() { + args := os.Args[1:] + if len(args) < 1 { + os.Exit(1) + } + switch args[0] { + case "local": + os.Args = append([]string{"lark-cli"}, args[1:]...) + os.Exit(cmd.Execute()) + case "context": + value, err := localContext() + if err != nil { + os.Exit(1) + } + _ = json.NewEncoder(os.Stdout).Encode(value) + return + case "local-health", "local-clear": + var err error + if args[0] == "local-clear" { + err = clearLocal() + } else { + err = localHealth() + } + if err != nil { + os.Exit(1) + } + return + } + bot := args[0] == "bot" + if !bot && args[0] != "user" { + os.Exit(1) + } + slug, kind := "lark", "oauth2" + provider := pluginauth.Provider{Export: exportUser, Detach: detach, Refresh: refresh, Revoke: revoke, Allowed: allowed, Run: runUser} + if bot { + slug, kind = "lark-app", "password" + provider = pluginauth.Provider{Export: exportBot, Allowed: allowed, Run: runBot} + } + if err := pluginauth.Serve(context.Background(), slug, kind, provider, args[1:]); err != nil { + fmt.Fprintln(os.Stderr, "plugin_auth_lark_failed") + os.Exit(1) + } +} diff --git a/plugins/lark/.wework-build/lark-auth/overlay/provider.go b/plugins/lark/.wework-build/lark-auth/overlay/provider.go new file mode 100644 index 0000000..96ed6f6 --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/overlay/provider.go @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/larksuite/cli/internal/auth" + "github.com/larksuite/cli/internal/core" + "github.com/larksuite/cli/internal/keychain" + pluginauth "github.com/larksuite/cli/internal/wegentpluginauth" +) + +var denied = errors.New("plugin_auth_lark_denied") +var client = &http.Client{Timeout: 25 * time.Second, Transport: &http.Transport{Proxy: nil}, CheckRedirect: func(*http.Request, []*http.Request) error { return denied }} + +func text(c pluginauth.Credential, key string) string { value, _ := c[key].(string); return value } +func brand(c pluginauth.Credential) (core.LarkBrand, error) { + value := text(c, "brand") + if value != "feishu" && value != "lark" { + return "", denied + } + return core.LarkBrand(value), nil +} +func validString(value string) bool { + return value != "" && len(value) < 8192 && !strings.ContainsAny(value, "\r\n\x00") +} +func validate(c pluginauth.Credential, bot bool) error { + if _, err := brand(c); err != nil { + return denied + } + if !validString(text(c, "app_id")) { + return denied + } + if bot { + if text(c, "username") != text(c, "app_id") || !validString(text(c, "password")) { + return denied + } + } else if !validString(text(c, "open_id")) || !validString(text(c, "access_token")) { + return denied + } + return nil +} +func accountID(c pluginauth.Credential, bot bool) string { + id := text(c, "brand") + ":" + text(c, "app_id") + if !bot { + id += ":" + text(c, "open_id") + } + return id +} +func userInfo(ctx context.Context, c pluginauth.Credential) error { + b, err := brand(c) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, "GET", core.ResolveEndpoints(b).Open+auth.PathUserInfoV1, nil) + if err != nil { + return denied + } + req.Header.Set("Authorization", "Bearer "+text(c, "access_token")) + result, err := requestJSON(req) + if err != nil { + return err + } + data, _ := result["data"].(map[string]any) + if data["open_id"] != text(c, "open_id") { + return denied + } + return nil +} +func requestJSON(req *http.Request) (map[string]any, error) { + response, err := client.Do(req) + if err != nil { + return nil, denied + } + defer response.Body.Close() + body, err := io.ReadAll(io.LimitReader(response.Body, 65537)) + if err != nil || len(body) > 65536 || response.StatusCode < 200 || response.StatusCode >= 300 { + return nil, denied + } + result := map[string]any{} + if len(body) > 0 && json.Unmarshal(body, &result) != nil { + return nil, denied + } + if result["error"] != nil && result["error"] != "" { + return nil, denied + } + if code, ok := result["code"].(float64); ok && code != 0 { + return nil, denied + } + return result, nil +} +func oauth(ctx context.Context, endpoint string, fields url.Values) (map[string]any, error) { + req, err := http.NewRequestWithContext(ctx, "POST", endpoint, strings.NewReader(fields.Encode())) + if err != nil { + return nil, denied + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // The caller owns uncertain rotating-token results. Never replay a refresh. + return requestJSON(req) +} +func sourceConfig() (*core.CliConfig, error) { + cfg, err := core.RequireConfig(keychain.Default()) + if err != nil || (cfg.Brand != core.BrandFeishu && cfg.Brand != core.BrandLark) || !validString(cfg.AppID) || !validString(cfg.AppSecret) { + return nil, denied + } + return cfg, nil +} +func exportUser(ctx context.Context) (pluginauth.Account, error) { + cfg, err := sourceConfig() + if err != nil { + return pluginauth.Account{}, err + } + token, raw, err := readToken(cfg.AppID, cfg.UserOpenId) + if err != nil || token == nil || token.AppId != cfg.AppID || token.UserOpenId != cfg.UserOpenId || token.RefreshToken == "" { + return pluginauth.Account{}, denied + } + c := pluginauth.Credential{"brand": string(cfg.Brand), "app_id": cfg.AppID, "open_id": cfg.UserOpenId, "access_token": token.AccessToken, "refresh_token": token.RefreshToken, "expires_at": token.ExpiresAt / 1000, "refresh_expires_at": token.RefreshExpiresAt / 1000, "scope": token.Scope, "provider_private": map[string]any{"app_secret": cfg.AppSecret, "source_fingerprint": fingerprint(raw)}} + if validate(c, false) != nil || userInfo(ctx, c) != nil { + return pluginauth.Account{}, denied + } + return pluginauth.Account{ID: accountID(c, false), Credential: c}, nil +} +func exportBot(ctx context.Context) (pluginauth.Account, error) { + cfg, err := sourceConfig() + if err != nil { + return pluginauth.Account{}, err + } + c := pluginauth.Credential{"brand": string(cfg.Brand), "app_id": cfg.AppID, "username": cfg.AppID, "password": cfg.AppSecret} + if _, err := botToken(ctx, c); err != nil { + return pluginauth.Account{}, err + } + return pluginauth.Account{ID: accountID(c, true), Credential: c}, nil +} +func botToken(ctx context.Context, c pluginauth.Credential) (string, error) { + if validate(c, true) != nil { + return "", denied + } + b, _ := brand(c) + result, err := oauth(ctx, core.ResolveEndpoints(b).Accounts+core.OAuthTokenV3Path, url.Values{"grant_type": {"client_credentials"}, "client_id": {text(c, "app_id")}, "client_secret": {text(c, "password")}}) + if err != nil { + return "", err + } + token, _ := result["access_token"].(string) + if !validString(token) { + return "", denied + } + return token, nil +} +func private(c pluginauth.Credential) (map[string]any, error) { + p, ok := c["provider_private"].(map[string]any) + if !ok { + return nil, denied + } + secret, _ := p["app_secret"].(string) + if !validString(secret) { + return nil, denied + } + return p, nil +} +func refresh(ctx context.Context, c pluginauth.Credential) (pluginauth.Account, error) { + if validate(c, false) != nil || !validString(text(c, "refresh_token")) { + return pluginauth.Account{}, denied + } + p, err := private(c) + if err != nil { + return pluginauth.Account{}, err + } + b, _ := brand(c) + result, err := oauth(ctx, auth.ResolveOAuthEndpoints(b).Token, url.Values{"grant_type": {"refresh_token"}, "client_id": {text(c, "app_id")}, "client_secret": {p["app_secret"].(string)}, "refresh_token": {text(c, "refresh_token")}}) + if err != nil { + return pluginauth.Account{}, err + } + access, _ := result["access_token"].(string) + expires, _ := result["expires_in"].(float64) + if !validString(access) || expires <= 0 || expires > 31536000 { + return pluginauth.Account{}, denied + } + next := pluginauth.Credential{} + for k, v := range c { + next[k] = v + } + next["access_token"], next["expires_at"] = access, time.Now().Unix()+int64(expires) + if value, ok := result["refresh_token"].(string); ok && value != "" { + next["refresh_token"] = value + } + if value, ok := result["refresh_token_expires_in"].(float64); ok && value > 0 { + next["refresh_expires_at"] = time.Now().Unix() + int64(value) + } + if value, ok := result["scope"].(string); ok && value != "" { + next["scope"] = value + } + return pluginauth.Account{ID: accountID(next, false), Credential: next}, nil +} +func revoke(ctx context.Context, c pluginauth.Credential) error { + if validate(c, false) != nil { + return denied + } + p, err := private(c) + if err != nil { + return err + } + b, _ := brand(c) + token, hint := text(c, "refresh_token"), "refresh_token" + if token == "" { + token, hint = text(c, "access_token"), "access_token" + } + _, err = oauth(ctx, auth.ResolveOAuthEndpoints(b).Revoke, url.Values{"client_id": {text(c, "app_id")}, "client_secret": {p["app_secret"].(string)}, "token": {token}, "token_type_hint": {hint}}) + return err +} +func localContext() (map[string]string, error) { + cfg, err := core.LoadMultiAppConfig() + if err != nil { + return nil, denied + } + app := cfg.CurrentAppConfig("") + if app == nil { + return nil, denied + } + if app.Brand != core.BrandFeishu && app.Brand != core.BrandLark { + return nil, denied + } + bot := string(app.Brand) + ":" + app.AppId + user := "" + if len(app.Users) > 0 { + user = bot + ":" + app.Users[0].UserOpenId + } + return map[string]string{"user": user, "bot": bot, "default_as": string(app.DefaultAs)}, nil +} +func localHealth() error { + cfg, err := sourceConfig() + if err != nil { + return err + } + token, err := auth.GetValidAccessToken(client, auth.NewUATCallOptions(cfg, io.Discard)) + if err != nil || token == "" { + return denied + } + return nil +} +func clearLocal() error { + cfg, err := core.LoadMultiAppConfig() + if err != nil { + return denied + } + app := cfg.CurrentAppConfig("") + if app == nil { + return denied + } + if len(app.Users) > 0 { + user := app.Users[0] + if err := keychain.Remove(keychain.LarkCliService, fmt.Sprintf("%s:%s", app.AppId, user.UserOpenId)); err != nil && !errors.Is(err, keychain.ErrNotFound) { + return denied + } + app.Users = app.Users[1:] + } + return core.SaveMultiAppConfig(cfg) +} diff --git a/plugins/lark/.wework-build/lark-auth/overlay/provider_test.go b/plugins/lark/.wework-build/lark-auth/overlay/provider_test.go new file mode 100644 index 0000000..1cd274f --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/overlay/provider_test.go @@ -0,0 +1,289 @@ +// SPDX-License-Identifier: MIT +package main + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + ext "github.com/larksuite/cli/extension/credential" + "github.com/larksuite/cli/internal/auth" + "github.com/larksuite/cli/internal/core" + "github.com/larksuite/cli/internal/keychain" + pluginauth "github.com/larksuite/cli/internal/wegentpluginauth" +) + +func TestOriginalBusinessCommandUsesMemoryAndNeverKeychain(t *testing.T) { + _, c := fixture(t) + delete(c, "refresh_token") + delete(c, "provider_private") + count := 0 + businessSeen := false + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + if r.URL.Path == "/open-apis/calendar/v4/calendars" { + businessSeen = true + } + w.Header().Set("Content-Type", "application/json") + if (r.URL.Path == auth.PathUserInfoV1 || r.URL.Path == "/open-apis/calendar/v4/calendars") && r.Header.Get("Authorization") != "Bearer synthetic-access" { + t.Errorf("missing memory credential on %s", r.URL.Path) + } + if r.URL.Path == auth.PathUserInfoV1 { + io.WriteString(w, `{"code":0,"data":{"open_id":"ou_synthetic"}}`) + } else { + io.WriteString(w, `{"code":0,"data":{"items":[]}}`) + } + })) + defer server.Close() + old := newBusinessTransport + newBusinessTransport = func(host string) *http.Transport { + return &http.Transport{Proxy: func(req *http.Request) (*url.URL, error) { return credentialOrigin(host, req) }, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, network, server.Listener.Addr().String()) + }, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} // Synthetic loopback server only. + } + defer func() { newBusinessTransport = old }() + keychain.WegentAccess = noKeychain{} + if err := runUser(context.Background(), c, []string{"api", "GET", "/open-apis/calendar/v4/calendars", "--as", "user"}); err != nil { + t.Fatal(err) + } + if count < 2 || !businessSeen { + t.Fatal("original CLI did not verify identity and execute business request") + } +} + +type memoryKeys struct { + values map[string]string + failAfterRemove bool + removes int +} + +func (k *memoryKeys) Get(_, key string) (string, error) { + value, ok := k.values[key] + if !ok { + return "", keychain.ErrNotFound + } + return value, nil +} +func (k *memoryKeys) Set(_, key, value string) error { k.values[key] = value; return nil } +func (k *memoryKeys) Remove(_, key string) error { + delete(k.values, key) + k.removes++ + if k.failAfterRemove { + return errors.New("synthetic failure") + } + return nil +} + +type roundTrip func(*http.Request) (*http.Response, error) + +func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } +func response(body string) *http.Response { + return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: http.Header{}} +} + +func fixture(t *testing.T) (*memoryKeys, pluginauth.Credential) { + t.Helper() + root := t.TempDir() + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", root) + t.Setenv("WEGENT_LARK_STATE_DIR", filepath.Join(root, "state")) + core.SetCurrentWorkspace(core.WorkspaceLocal) + keys := &memoryKeys{values: map[string]string{}} + previous := keychain.WegentAccess + keychain.WegentAccess = keys + t.Cleanup(func() { keychain.WegentAccess = previous }) + cfg := &core.MultiAppConfig{CurrentApp: "cli_synthetic", Apps: []core.AppConfig{{AppId: "cli_synthetic", AppSecret: core.PlainSecret("synthetic-app-secret"), Brand: core.BrandFeishu, Users: []core.AppUser{{UserOpenId: "ou_synthetic"}}}}} + if err := core.SaveMultiAppConfig(cfg); err != nil { + t.Fatal(err) + } + token := &auth.StoredUAToken{AppId: "cli_synthetic", UserOpenId: "ou_synthetic", AccessToken: "synthetic-access", RefreshToken: "synthetic-refresh", ExpiresAt: time.Now().Add(time.Hour).UnixMilli(), RefreshExpiresAt: time.Now().Add(24 * time.Hour).UnixMilli(), Scope: "contact:user.base:readonly"} + data, _ := json.Marshal(token) + keys.values["cli_synthetic:ou_synthetic"] = string(data) + oldClient := client + client = &http.Client{Transport: roundTrip(func(req *http.Request) (*http.Response, error) { + if req.URL.Host != "open.feishu.cn" || req.Header.Get("Authorization") != "Bearer synthetic-access" { + t.Errorf("unexpected provider request") + } + return response(`{"code":0,"data":{"open_id":"ou_synthetic"}}`), nil + })} + t.Cleanup(func() { client = oldClient }) + account, err := exportUser(context.Background()) + if err != nil { + t.Fatal(err) + } + return keys, account.Credential +} +func TestExportDoesNotMutateAndKeepsRefreshSecretsPrivate(t *testing.T) { + keys, c := fixture(t) + if keys.removes != 0 || c["refresh_token"] != "synthetic-refresh" { + t.Fatal("unexpected source mutation") + } + if c["app_secret"] != nil || c["provider_private"].(map[string]any)["app_secret"] != "synthetic-app-secret" { + t.Fatal("wrong secret boundary") + } +} +func TestDetachRecoveryAndReplay(t *testing.T) { + keys, c := fixture(t) + id := strings.Repeat("a", 64) + keys.failAfterRemove = true + if detach(context.Background(), id, c) == nil { + t.Fatal("cleanup interruption must fail") + } + keys.failAfterRemove = false + if err := detach(context.Background(), id, c); err != nil { + t.Fatal(err) + } + if err := detach(context.Background(), id, c); err != nil { + t.Fatal(err) + } + if keys.removes != 1 { + t.Fatal("replay repeated deletion") + } + root, _ := stateRoot() + files, _ := os.ReadDir(root) + for _, file := range files { + data, _ := os.ReadFile(filepath.Join(root, file.Name())) + if strings.Contains(string(data), "synthetic-access") || strings.Contains(string(data), "synthetic-refresh") || strings.Contains(string(data), "synthetic-app-secret") { + t.Fatal("secret in receipt") + } + } +} +func TestChangedSourceAbortsOldHandoffDurably(t *testing.T) { + keys, c := fixture(t) + old := keys.values["cli_synthetic:ou_synthetic"] + keys.values["cli_synthetic:ou_synthetic"] = strings.ReplaceAll(old, "synthetic-access", "new-access") + id := strings.Repeat("b", 64) + if !errors.Is(detach(context.Background(), id, c), pluginauth.ErrSourceChanged) { + t.Fatal("expected source changed") + } + keys.values["cli_synthetic:ou_synthetic"] = old + if !errors.Is(detach(context.Background(), id, c), pluginauth.ErrSourceChanged) || keys.removes != 0 { + t.Fatal("old transfer can remove new login") + } +} +func TestCorruptSourceIsNotTreatedAsMissing(t *testing.T) { + keys, c := fixture(t) + keys.values["cli_synthetic:ou_synthetic"] = "not-json" + if detach(context.Background(), strings.Repeat("c", 64), c) == nil || keys.removes != 0 { + t.Fatal("corrupt source accepted") + } +} +func TestRefreshIsSingleRequestAndDoesNotWriteSource(t *testing.T) { + keys, c := fixture(t) + before := keys.values["cli_synthetic:ou_synthetic"] + count := 0 + client = &http.Client{Transport: roundTrip(func(req *http.Request) (*http.Response, error) { + count++ + body, _ := io.ReadAll(req.Body) + values, _ := url.ParseQuery(string(body)) + if req.URL.String() != "https://open.feishu.cn/open-apis/authen/v2/oauth/token" || values.Get("client_secret") != "synthetic-app-secret" || values.Get("refresh_token") != "synthetic-refresh" { + t.Fatal("incorrect refresh request") + } + return response(`{"access_token":"new-access","refresh_token":"new-refresh","expires_in":3600}`), nil + })} + next, err := refresh(context.Background(), c) + if err != nil || count != 1 || next.Credential["refresh_token"] != "new-refresh" || keys.values["cli_synthetic:ou_synthetic"] != before { + t.Fatal("refresh contract") + } + count = 0 + client.Transport = roundTrip(func(*http.Request) (*http.Response, error) { count++; return nil, errors.New("synthetic secret body") }) + if _, err = refresh(context.Background(), c); err == nil || count != 1 { + t.Fatal("uncertain refresh retried") + } +} +func TestUserAndBotProvidersCannotCrossIdentities(t *testing.T) { + _, c := fixture(t) + for _, bot := range []bool{false, true} { + p := &memoryProvider{c, "synthetic-memory-token", bot} + account, err := p.ResolveAccount(context.Background()) + if err != nil || account.AppSecret != "" { + t.Fatal("app secret supplied to business tree") + } + correct, wrong := ext.TokenTypeUAT, ext.TokenTypeTAT + if bot { + correct, wrong = wrong, correct + } + if token, err := p.ResolveToken(context.Background(), ext.TokenSpec{Type: correct, AppID: "cli_synthetic"}); err != nil || token.Value != "synthetic-memory-token" { + t.Fatal("missing memory token") + } + if _, err := p.ResolveToken(context.Background(), ext.TokenSpec{Type: wrong}); err == nil { + t.Fatal("identity escalation") + } + } +} +func TestManagedRunRefusesManagementSecretsAndCommands(t *testing.T) { + _, c := fixture(t) + if runUser(context.Background(), c, []string{"docs", "--help"}) == nil { + t.Fatal("refresh secret accepted") + } + for _, args := range [][]string{{"auth", "login"}, {"config", "show"}, {"docs", "--profile=other"}, {"api", "--base-url=https://outside.invalid"}, {"im", "--debug"}} { + if allowed(args) { + t.Fatalf("unsafe arguments: %v", args) + } + } + if !allowed([]string{"im", "--help"}) || !allowed([]string{"im", "messages", "create", "--yes"}) { + t.Fatal("ordinary commands or explicit confirmation lost") + } +} +func TestOriginGuardSurvivesTransportCloning(t *testing.T) { + transport := &http.Transport{Proxy: func(req *http.Request) (*url.URL, error) { return credentialOrigin("open.feishu.cn", req) }} + clone := transport.Clone() + for _, target := range []string{"https://outside.invalid", "http://open.feishu.cn", "https://open.feishu.cn.evil.invalid"} { + req, _ := http.NewRequest("GET", target, nil) + req.Header.Set("Authorization", "Bearer synthetic") + if _, err := clone.Proxy(req); err == nil { + t.Fatal("credential origin accepted") + } + } + req, _ := http.NewRequest("GET", "https://open.feishu.cn/open-apis", nil) + req.Header.Set("Authorization", "Bearer synthetic") + if _, err := clone.Proxy(req); err != nil { + t.Fatal(err) + } +} +func TestRevokeUsesPrivateClientSecretAndRefreshToken(t *testing.T) { + _, c := fixture(t) + count := 0 + client = &http.Client{Transport: roundTrip(func(req *http.Request) (*http.Response, error) { + count++ + body, _ := io.ReadAll(req.Body) + fields, _ := url.ParseQuery(string(body)) + if req.URL.Host != "accounts.feishu.cn" || fields.Get("token_type_hint") != "refresh_token" || fields.Get("client_secret") != "synthetic-app-secret" { + t.Fatal("wrong revoke request") + } + return response(`{}`), nil + })} + if revoke(context.Background(), c) != nil || count != 1 { + t.Fatal("revoke failed") + } +} + +func TestLocalLogoutPreservesOtherUsers(t *testing.T) { + keys, _ := fixture(t) + cfg, err := core.LoadMultiAppConfig() + if err != nil { + t.Fatal(err) + } + cfg.Apps[0].Users = append(cfg.Apps[0].Users, core.AppUser{UserOpenId: "ou_other"}) + if err := core.SaveMultiAppConfig(cfg); err != nil { + t.Fatal(err) + } + keys.values["cli_synthetic:ou_other"] = "unrelated" + if err := clearLocal(); err != nil { + t.Fatal(err) + } + cfg, err = core.LoadMultiAppConfig() + if err != nil || len(cfg.Apps[0].Users) != 1 || keys.values["cli_synthetic:ou_other"] != "unrelated" { + t.Fatal("removed unrelated user") + } +} diff --git a/plugins/lark/.wework-build/lark-auth/overlay/source.go b/plugins/lark/.wework-build/lark-auth/overlay/source.go new file mode 100644 index 0000000..5ad794c --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/overlay/source.go @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MIT +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "os" + "path/filepath" + "runtime" + + "github.com/larksuite/cli/internal/auth" + "github.com/larksuite/cli/internal/keychain" + pluginauth "github.com/larksuite/cli/internal/wegentpluginauth" +) + +func fingerprint(value string) string { + sum := sha256.Sum256([]byte(value)) + return hex.EncodeToString(sum[:]) +} +func stateRoot() (string, error) { + root := os.Getenv("WEGENT_LARK_STATE_DIR") + if !filepath.IsAbs(root) { + return "", denied + } + if err := os.MkdirAll(root, 0700); err != nil { + return "", denied + } + return root, nil +} +func atomicJSON(path string, value any) error { + data, err := json.Marshal(value) + if err != nil { + return denied + } + file, err := os.CreateTemp(filepath.Dir(path), ".receipt-") + if err != nil { + return denied + } + defer os.Remove(file.Name()) + if _, err = file.Write(data); err != nil { + file.Close() + return denied + } + if err = file.Sync(); err != nil { + file.Close() + return denied + } + if err = file.Close(); err != nil { + return denied + } + if err = os.Rename(file.Name(), path); err != nil { + return denied + } + if runtime.GOOS != "windows" { + directory, err := os.Open(filepath.Dir(path)) + if err != nil { + return denied + } + defer directory.Close() + if directory.Sync() != nil { + return denied + } + } + return nil +} +func readToken(app, user string) (*auth.StoredUAToken, string, error) { + if !validString(app) || !validString(user) { + return nil, "", denied + } + value, err := keychain.Get(keychain.LarkCliService, app+":"+user) + if errors.Is(err, keychain.ErrNotFound) || (err == nil && value == "") { + return nil, "", nil + } + if err != nil || len(value) > 65536 { + return nil, "", denied + } + token := &auth.StoredUAToken{} + if json.Unmarshal([]byte(value), token) != nil { + return nil, "", denied + } + return token, value, nil +} +func writeManaged(account string) error { + root, err := stateRoot() + if err != nil { + return err + } + return atomicJSON(filepath.Join(root, "account-"+fingerprint(account)+".json"), map[string]string{"account_id": account}) +} + +type receipt struct { + State string `json:"state"` + Fingerprint string `json:"fingerprint"` +} + +func detach(ctx context.Context, id string, c pluginauth.Credential) error { + if validate(c, false) != nil { + return denied + } + p, err := private(c) + if err != nil { + return err + } + expected, _ := p["source_fingerprint"].(string) + if len(expected) != 64 { + return denied + } + root, err := stateRoot() + if err != nil { + return err + } + path := filepath.Join(root, "transfer-"+id+".json") + previous := receipt{} + if data, err := os.ReadFile(path); err == nil { + if json.Unmarshal(data, &previous) != nil || previous.Fingerprint != expected { + return denied + } + if previous.State == "aborted" { + return pluginauth.ErrSourceChanged + } + if previous.State == "detached" { + return nil + } + if previous.State != "pending" { + return denied + } + } else if !errors.Is(err, os.ErrNotExist) { + return denied + } + token, raw, err := readToken(text(c, "app_id"), text(c, "open_id")) + if err != nil { + return err + } + if fingerprint(raw) != expected && !(previous.State == "pending" && token == nil) { + if atomicJSON(path, receipt{"aborted", expected}) != nil { + return denied + } + return pluginauth.ErrSourceChanged + } + if atomicJSON(path, receipt{"pending", expected}) != nil { + return denied + } + // Only remove the exact selected UAT. App credentials and other users remain. + // Unlike upstream auth logout, this must never revoke the exported grant. + if token != nil { + if keychain.Remove(keychain.LarkCliService, text(c, "app_id")+":"+text(c, "open_id")) != nil { + return denied + } + if next, _, err := readToken(text(c, "app_id"), text(c, "open_id")); err != nil || next != nil { + return denied + } + } + if writeManaged(accountID(c, false)) != nil { + return denied + } + return atomicJSON(path, receipt{"detached", expected}) +} diff --git a/plugins/lark/.wework-build/lark-auth/package.py b/plugins/lark/.wework-build/lark-auth/package.py new file mode 100644 index 0000000..556e4f2 --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/package.py @@ -0,0 +1,103 @@ +"""Package the same verified companion for each supported desktop/cloud target.""" + +import argparse +import hashlib +import json +import os +import shutil +import stat +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +from build import build, prepare, source_archive +from toolchain import ensure_go +from verify import verify_package + +TARGETS = ( + "darwin/arm64", + "darwin/amd64", + "linux/amd64", + "linux/arm64", + "windows/amd64", +) + + +def assemble(plugin, output, provided=None): + with tempfile.TemporaryDirectory(prefix="wegent-lark-package-") as temporary: + directory = Path(temporary) + ensure_go(directory) + source = prepare(directory, source_archive(directory, provided)) + subprocess.run( + ["go", "test", "./internal/wegentpluginauth", "./cmd/wegent-account-auth"], + cwd=source, + check=True, + ) + staged = directory / "plugin" + shutil.copytree( + plugin, + staged, + symlinks=True, + ignore=shutil.ignore_patterns( + "__pycache__", "*.pyc", ".DS_Store", "native" + ), + ) + for path in staged.rglob("*"): + if path.is_symlink() or path.name in {".env", ".git"}: + raise ValueError("Plugin contains private state or a symlink") + manifest = json.loads( + (staged / ".codex-plugin/plugin.json").read_text(encoding="utf-8") + ) + if ( + manifest["name"] != "lark" + or manifest["connectors"][0]["accountAuth"]["exportMode"] != "exclusive" + ): + raise ValueError("Incorrect Lark auth declaration") + for target in TARGETS: + folder = staged / "scripts/native" / target.replace("/", "-") + build(source, folder / "lark-account-auth.xz", target) + shutil.copyfile(source / "LICENSE", folder / "UPSTREAM-LICENSE") + verify_package(staged) + output.parent.mkdir(parents=True, exist_ok=True) + files = sorted(path for path in staged.rglob("*") if path.is_file()) + if sum(path.stat().st_size for path in files) > 200 * 1024 * 1024: + raise ValueError("Expanded plugin exceeds package limit") + candidate = directory / "plugin.zip" + with zipfile.ZipFile( + candidate, "w", zipfile.ZIP_DEFLATED, compresslevel=9 + ) as archive: + for path in files: + entry = zipfile.ZipInfo( + path.relative_to(staged).as_posix(), (1980, 1, 1, 0, 0, 0) + ) + entry.create_system = 3 + entry.external_attr = (stat.S_IFREG | 0o644) << 16 + entry.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(entry, path.read_bytes(), compresslevel=9) + if candidate.stat().st_size > 50 * 1024 * 1024: + raise ValueError("Plugin archive exceeds upload limit") + shutil.copyfile(candidate, output) + checksum = hashlib.sha256(output.read_bytes()).hexdigest() + output.with_name(output.name + ".sha256").write_text( + checksum + " " + output.name + "\n" + ) + print( + json.dumps( + { + "artifact": str(output), + "sha256": checksum, + "bytes": output.stat().st_size, + } + ) + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--plugin", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--source-archive", type=Path) + args = parser.parse_args() + assemble(args.plugin.resolve(), args.output.resolve(), args.source_archive) diff --git a/plugins/lark/.wework-build/lark-auth/toolchain.py b/plugins/lark/.wework-build/lark-auth/toolchain.py new file mode 100644 index 0000000..e6834eb --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/toolchain.py @@ -0,0 +1,86 @@ +"""Use the pinned Go compiler, bootstrapping the Linux CI image when needed.""" + +import hashlib +import os +import platform +import shutil +import subprocess +import tarfile +import urllib.request +import zipfile +from pathlib import Path + +GO_VERSION = "go1.25.9" +ARCHIVES = { + "darwin/amd64": { + "filename": "go1.25.9.darwin-amd64.tar.gz", + "sha256": "92cb78fba4796e218c1accb0ea0a214ef2094c382049a244ad6505505d015fbe", + }, + "darwin/arm64": { + "filename": "go1.25.9.darwin-arm64.tar.gz", + "sha256": "9528be7329b9770631a6bd09ca2f3a73ed7332bec01d87435e75e92d8f130363", + }, + "linux/amd64": { + "filename": "go1.25.9.linux-amd64.tar.gz", + "sha256": "00859d7bd6defe8bf84d9db9e57b9a4467b2887c18cd93ae7460e713db774bc1", + }, + "linux/arm64": { + "filename": "go1.25.9.linux-arm64.tar.gz", + "sha256": "ec342e7389b7f489564ed5463c63b16cf8040023dabc7861256677165a8c0e2b", + }, + "windows/amd64": { + "filename": "go1.25.9.windows-amd64.zip", + "sha256": "a7a710e225467b34e9e09fb432b829c86c9b2da5821ee5418f7eb2e8ae1a22cc", + }, +} + + +def ensure_go(directory: Path) -> None: + executable = shutil.which("go") + if executable: + probe_environment = {k: v for k, v in os.environ.items() if k != "GOROOT"} + probe_environment.update({"GOENV": "off", "GOTOOLCHAIN": "local"}) + result = subprocess.run( + [executable, "env", "GOVERSION", "GOROOT"], + env=probe_environment, + capture_output=True, + text=True, + check=True, + ) + installed = result.stdout.strip().splitlines() + if len(installed) == 2 and installed[0] == GO_VERSION: + os.environ["GOROOT"] = installed[1] + os.environ["GOTOOLCHAIN"] = "local" + return + system = {"Darwin": "darwin", "Linux": "linux", "Windows": "windows"}.get( + platform.system() + ) + arch = { + "x86_64": "amd64", + "AMD64": "amd64", + "aarch64": "arm64", + "arm64": "arm64", + }.get(platform.machine()) + release = ARCHIVES.get(f"{system}/{arch}") + if release is None: + raise ValueError(f"Unsupported Go build host: {system}/{arch}") + archive = directory / release["filename"] + with urllib.request.urlopen( + f"https://go.dev/dl/{release['filename']}", timeout=60 + ) as response: + data = response.read(80 * 1024 * 1024 + 1) + if hashlib.sha256(data).hexdigest() != release["sha256"]: + raise ValueError("Go toolchain checksum mismatch") + archive.write_bytes(data) + if archive.suffix == ".zip": + with zipfile.ZipFile(archive) as source: + source.extractall(directory) + else: + with tarfile.open(archive) as source: + source.extractall(directory, filter="data") + os.environ["PATH"] = ( + str(directory / "go/bin") + os.pathsep + os.environ.get("PATH", "") + ) + # The selected compiler and standard library must come from the same archive. + os.environ["GOROOT"] = str(directory / "go") + os.environ["GOTOOLCHAIN"] = "local" diff --git a/plugins/lark/.wework-build/lark-auth/verify.py b/plugins/lark/.wework-build/lark-auth/verify.py new file mode 100644 index 0000000..bbffe38 --- /dev/null +++ b/plugins/lark/.wework-build/lark-auth/verify.py @@ -0,0 +1,111 @@ +"""Check native inventory, child status, and public broker routing from the artifact.""" + +import hashlib +import json +import os +import socket +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + + +def verify_package(plugin): + inventory = list((plugin / "scripts/native").glob("*/lark-account-auth.json")) + expected = { + "darwin-amd64", + "darwin-arm64", + "linux-amd64", + "linux-arm64", + "windows-amd64", + } + if {p.parent.name for p in inventory} != expected: + raise ValueError("Incomplete native platform inventory") + for metadata in inventory: + value = json.loads(metadata.read_text()) + blob = metadata.with_suffix(".xz") + if hashlib.sha256(blob.read_bytes()).hexdigest() != value["compressedSha256"]: + raise ValueError("Native artifact checksum mismatch") + with tempfile.TemporaryDirectory() as temporary: + home = Path(temporary) + env = { + k: v + for k, v in os.environ.items() + if not k.startswith(("WEGENT_", "LARK", "OPENCLAW", "HERMES")) + } + env.update( + HOME=str(home), + USERPROFILE=str(home), + WEGENT_EXECUTOR_HOME=str(home / "executor"), + PYTHONDONTWRITEBYTECODE="1", + ) + # Exercise the packaged launcher and private channel with an invalid + # business credential; never touch a real account or system keychain. + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + listener.listen(1) + listener.settimeout(30) + env["WEGENT_PLUGIN_AUTH_PORT"] = str(listener.getsockname()[1]) + process = subprocess.Popen( + [ + sys.executable, + str(plugin / "scripts/account-auth.py"), + "run", + "account-status", + ], + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + try: + process.stdin.write(b"x" * 32) + process.stdin.close() + process.stdin = None + connection, _ = listener.accept() + with connection: + connection.settimeout(15) + with connection.makefile("rwb", buffering=0) as stream: + nonce = b"" + while len(nonce) < 32: + chunk = stream.read(32 - len(nonce)) + if not chunk: + raise ValueError("Truncated native nonce") + nonce += chunk + if nonce != b"x" * 32: + raise ValueError("Native nonce mismatch") + payload = json.dumps( + { + "protocolVersion": 1, + "connectorSlug": "lark", + "credentialType": "oauth2", + "credential": { + "access_token": "synthetic-private-token", + "brand": "invalid", + }, + } + ).encode() + stream.write(struct.pack(">I", len(payload)) + payload) + out, err = process.communicate(timeout=20) + if process.returncode == 0 or b"synthetic-private-token" in out + err: + raise ValueError( + "Packaged adapter accepted or exposed an invalid credential" + ) + finally: + if process.poll() is None: + process.kill() + process.communicate(timeout=5) + subprocess.run( + [ + sys.executable, + "-m", + "unittest", + "discover", + "-s", + str(plugin / "scripts/tests"), + "-v", + ], + env=env, + check=True, + ) diff --git a/plugins/lark/.wework-build/plugin-auth-go/LICENSE b/plugins/lark/.wework-build/plugin-auth-go/LICENSE new file mode 100644 index 0000000..b8c67ae --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2025 Weibo, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/plugins/lark/.wework-build/plugin-auth-go/README.en.md b/plugins/lark/.wework-build/plugin-auth-go/README.en.md new file mode 100644 index 0000000..1481a0b --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/README.en.md @@ -0,0 +1,45 @@ +--- +sidebar_position: 1 +--- + +# Go plugin authentication SDK + +Version `0.3.0` implements the native private channel for `accountAuth` draft v1. +Implement `Provider` callbacks and call `Serve`; the DWS companion is the first +integration. The SDK owns the nonce handshake, loopback transport, framing and +identity checks, suppressed auth output and fixed errors. Providers own public +business output and must reject credential overrides, auth commands and debugging +through `Allowed`. + +Refresh returns a complete `Account`. Unlike Python's incremental merge API, Go +providers preserve unchanged account fields and non-rotated refresh tokens. The +backend also validates account identity. Keep extra grant-management material in +the `provider_private` object, which is excluded from business credentials along +with refresh tokens, client secrets and persistent codes. + +Optional `Detach(ctx, migrationID, credential)` runs only after durable encrypted +escrow. Return nil only after an idempotent, recoverable source-store handoff. +Never delete source credentials in `Export`. The declaration +`exportMode: "exclusive"` selects stage → detach → activate in the native host. +Do not declare this capability without a supported source-store transfer API. + +Return `ErrSourceChanged` only after durably fencing off the old migration ID +under the provider lock, preventing every delayed detach from deleting source +credentials. The host then cancels obsolete escrow; a user retry exports current +credentials with a new ID. Persistence failures or uncertain source state must +return an ordinary error and retain recoverable escrow. + +Declare custom source directories and public switches as `directory` or bounded +`enum` entries in `accountAuth.localEnvironment`. Read host-validated settings +with `LocalConfiguration()` in `Export`, `Authorize` or `Detach`. They are not +merged into the process environment or provided to `Run`, `Refresh` or `Revoke`. +Do not include local paths in exported credentials. See the +[Python SDK](../plugin-auth/README.en.md) for the declaration and limits. + +The SDK does not yet include public CLI broker delegation and does not own +Keychain storage, device grants or provider HTTP protocols. + +```bash +cd sdk/plugin-auth-go +go test -race ./... +``` diff --git a/plugins/lark/.wework-build/plugin-auth-go/README.md b/plugins/lark/.wework-build/plugin-auth-go/README.md new file mode 100644 index 0000000..3257d4a --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/README.md @@ -0,0 +1,38 @@ +--- +sidebar_position: 1 +--- + +# Go 插件认证 SDK + +版本 `0.3.0`,实现 `accountAuth` 草案 v1 的原生私有通道。提供方实现 +`Provider` 的 `Export` / `Authorize` / `Refresh` / `Revoke` / `Allowed` / `Run` +回调,调用 `Serve` 即可;DWS companion 是首个接入实例。 + +SDK 负责一次性 nonce、仅回环 TCP、长度和身份校验、认证回调输出抑制、固定错误。 +`Run` 的公开业务输出由插件负责,`Allowed` 必须拒绝认证管理、令牌覆盖与调试参数。 +刷新回调返回完整 `Account`;与 Python SDK 的增量合并接口不同,Go 提供方必须 +保留稳定账号字段与未轮换的 Refresh Token,后端还会校验账号未改变。 + +`Detach(ctx, migrationID, credential)` 是独占迁移的可选回调:只能在后端已持久化 +暂存后由原生宿主调用,必须完成可恢复、幂等的源存储交接才返回 nil。 +不要在 `Export` 内删除源凭据。声明 `exportMode: "exclusive"` 后,宿主会走 +暂存→detach→激活流程;没有受支持的迁移接口时不要声明该能力。 + +源凭据轮换时,只有在提供方锁内持久化旧迁移 ID 的作废记录、阻止所有迟到操作 +删除凭据后,才能返回 `ErrSourceChanged`。宿主随后取消旧暂存,用户可使用新 ID +迁移最新凭据。记录无法持久化或源状态不明时返回普通错误,保留可恢复暂存。 + +源存储有自定义目录或公开开关时,在 `accountAuth.localEnvironment` 声明 +`directory` 或有限 `enum`,通过 `LocalConfiguration()` 读取宿主校验后的配置。 +配置仅提供给 `Export`、`Authorize` 和 `Detach`,不会自动合并到进程环境; +`Run`、`Refresh` 和 `Revoke` 不接收源设备配置。不要把本机目录加入导出的凭据。 +声明格式和边界见 [Python SDK](../plugin-auth/README.md)。 + +仅刷新、授权、撤销需要的额外材料放在 `provider_private` 对象内,后端不会向 +业务回调下发该对象、Refresh Token、`client_secret` 或 `persistent_code`。 +该库暂不提供公开 CLI 的 broker 委派接口,也不负责钥匙串、设备授权或提供方网络协议。 + +```bash +cd sdk/plugin-auth-go +go test -race ./... +``` diff --git a/plugins/lark/.wework-build/plugin-auth-go/adapter.go b/plugins/lark/.wework-build/plugin-auth-go/adapter.go new file mode 100644 index 0000000..d863aff --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/adapter.go @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +package pluginauth + +import ( + "context" + "encoding/json" + "errors" + "os" + "strings" +) + +var ErrProvider = errors.New("plugin_auth_provider_failed") +var ErrUnsupported = errors.New("plugin_auth_operation_unsupported") + +// ErrSourceChanged is valid only after Detach durably prevents this transfer ID +// from deleting source credentials. It authorizes clearing the obsolete escrow. +var ErrSourceChanged = errors.New("plugin_auth_source_changed") + +type Account struct { + ID string + Credential Credential +} + +// Provider callbacks own provider semantics; transport and secret error handling are shared. +type Provider struct { + Export func(context.Context) (Account, error) + Authorize func(context.Context) (Account, error) + Refresh func(context.Context, Credential) (Account, error) + Revoke func(context.Context, Credential) error + Detach func(context.Context, string, Credential) error + Allowed func([]string) bool + Run func(context.Context, Credential, []string) error +} + +func validate(value Credential, kind string) error { + fields := map[string][]string{"password": {"username", "password"}, "bearer": {"token"}, "oauth2": {"access_token"}} + required, ok := fields[kind] + if !ok || value == nil { + return ErrProtocol + } + for _, key := range required { + text, ok := value[key].(string) + if !ok || strings.TrimSpace(text) == "" { + return ErrProtocol + } + } + return nil +} + +// Serve runs once per native process. It is intentionally not goroutine-safe: +// auth callback stdout/stderr are redirected process-wide, just as in the Python SDK. +func Serve(ctx context.Context, connector, kind string, provider Provider, arguments []string) (err error) { + defer func() { + if recover() != nil { + err = ErrProvider + } + }() + if len(arguments) == 0 { + return ErrUnsupported + } + operation := arguments[0] + switch operation { + case "export": + if provider.Export == nil || len(arguments) != 1 { + return ErrUnsupported + } + case "authorize": + if kind != "oauth2" || provider.Authorize == nil || len(arguments) != 1 { + return ErrUnsupported + } + case "refresh": + if kind != "oauth2" || provider.Refresh == nil || len(arguments) != 1 { + return ErrUnsupported + } + case "revoke": + if kind != "oauth2" || provider.Revoke == nil || len(arguments) != 1 { + return ErrUnsupported + } + case "detach": + if kind != "oauth2" || provider.Detach == nil || len(arguments) != 2 || len(arguments[1]) != 64 || strings.Trim(arguments[1], "0123456789abcdef") != "" { + return ErrUnsupported + } + case "run": + if provider.Run == nil || provider.Allowed == nil || !provider.Allowed(arguments[1:]) { + return ErrUnsupported + } + default: + return ErrUnsupported + } + channel, err := Connect(connector, kind) + if err != nil { + return err + } + defer channel.Close() + var credential Credential + if operation == "run" || operation == "refresh" || operation == "revoke" || operation == "detach" { + credential, err = channel.Receive() + if err != nil { + return err + } + } + if operation == "run" { + channel.Close() + if provider.Run(ctx, credential, arguments[1:]) != nil { + return ErrProvider + } + return nil + } + var account Account + err = quiet(func() error { + switch operation { + case "export": + account, err = provider.Export(ctx) + case "authorize": + account, err = provider.Authorize(ctx) + case "refresh": + account, err = provider.Refresh(ctx, credential) + case "revoke": + err = provider.Revoke(ctx, credential) + case "detach": + err = provider.Detach(ctx, arguments[1], credential) + } + return err + }) + if err != nil { + if operation == "detach" && errors.Is(err, ErrSourceChanged) { + return json.NewEncoder(os.Stdout).Encode(map[string]any{"status": "source_changed", "protocolVersion": 1}) + } + return ErrProvider + } + if operation == "revoke" { + return json.NewEncoder(os.Stdout).Encode(map[string]any{"status": "ok", "protocolVersion": 1}) + } + if operation == "detach" { + return json.NewEncoder(os.Stdout).Encode(map[string]any{"status": "detached", "protocolVersion": 1}) + } + if strings.TrimSpace(account.ID) == "" || len(account.ID) > 256 { + return ErrProvider + } + if err := channel.Send(account.Credential); err != nil { + return err + } + return json.NewEncoder(os.Stdout).Encode(map[string]any{"status": "ok", "protocolVersion": 1, "credentialType": kind, "accountId": account.ID}) +} + +func quiet(callback func() error) error { + sink, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + if err != nil { + return ErrProvider + } + stdout, stderr := os.Stdout, os.Stderr + os.Stdout, os.Stderr = sink, sink + defer func() { os.Stdout, os.Stderr = stdout, stderr; sink.Close() }() + return callback() +} diff --git a/plugins/lark/.wework-build/plugin-auth-go/configuration.go b/plugins/lark/.wework-build/plugin-auth-go/configuration.go new file mode 100644 index 0000000..e38f65e --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/configuration.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +package pluginauth + +import ( + "errors" + "os" + "regexp" + "strings" +) + +var ErrLocalConfiguration = errors.New("plugin_auth_invalid_local_configuration") + +// LocalConfiguration returns host-validated source settings without modifying +// the interpreter environment. Run, refresh and revoke receive no source settings. +func LocalConfiguration() (map[string]string, error) { + raw, present := os.LookupEnv("WEGENT_PLUGIN_AUTH_LOCAL_CONFIGURATION") + if !present { + return map[string]string{}, nil + } + if len(raw) > 16384 { + return nil, ErrLocalConfiguration + } + value, err := strictJSON([]byte(raw)) + object, ok := value.(map[string]any) + if err != nil || !ok || len(object) > 16 { + return nil, ErrLocalConfiguration + } + result := make(map[string]string, len(object)) + for name, value := range object { + validName, _ := regexp.MatchString(`^[A-Z][A-Z0-9_]{0,63}$`, name) + setting, ok := value.(string) + if !validName || !ok || setting == "" || len(setting) > 4096 || strings.ContainsRune(setting, 0) { + return nil, ErrLocalConfiguration + } + result[name] = setting + } + return result, nil +} diff --git a/plugins/lark/.wework-build/plugin-auth-go/configuration_test.go b/plugins/lark/.wework-build/plugin-auth-go/configuration_test.go new file mode 100644 index 0000000..de6fc63 --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/configuration_test.go @@ -0,0 +1,23 @@ +package pluginauth + +import ( + "os" + "strings" + "testing" +) + +func TestLocalConfigurationIsBoundedAndNeverOverridesEnvironment(t *testing.T) { + const key = "WEGENT_PLUGIN_AUTH_LOCAL_CONFIGURATION" + originalPath := os.Getenv("PATH") + t.Setenv(key, `{"PATH":"/synthetic/provider"}`) + value, err := LocalConfiguration() + if err != nil || value["PATH"] != "/synthetic/provider" || os.Getenv("PATH") != originalPath { + t.Fatal("configuration was not isolated") + } + for _, raw := range []string{`[]`, `{"MODE":1}`, `{"MODE":"a","MODE":"b"}`, `{"mixedCase":"a"}`, strings.Repeat("x", 16385)} { + t.Setenv(key, raw) + if _, err := LocalConfiguration(); err != ErrLocalConfiguration { + t.Fatal("invalid configuration was accepted") + } + } +} diff --git a/plugins/lark/.wework-build/plugin-auth-go/go.mod b/plugins/lark/.wework-build/plugin-auth-go/go.mod new file mode 100644 index 0000000..40a9a63 --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/go.mod @@ -0,0 +1,3 @@ +module github.com/wegent/plugin-auth-go + +go 1.23 diff --git a/plugins/lark/.wework-build/plugin-auth-go/transport.go b/plugins/lark/.wework-build/plugin-auth-go/transport.go new file mode 100644 index 0000000..b2610e6 --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/transport.go @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 +// Package pluginauth implements the native accountAuth channel for embedded CLIs. +package pluginauth + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "io" + "math" + "net" + "os" + "strconv" + "time" + "unicode/utf8" +) + +const MaxFrameBytes = 65536 +const Version = "0.3.0" + +var ErrProtocol = errors.New("plugin_auth_invalid_transport") + +type Credential map[string]any + +// Channel carries provider credentials only on a capability-authenticated socket. +type Channel struct { + net.Conn + Connector, CredentialType string +} + +func Connect(connector, credentialType string) (*Channel, error) { + if os.Getenv("WEGENT_PLUGIN_AUTH_FD") != "" { + return nil, ErrProtocol + } + port, err := strconv.ParseUint(os.Getenv("WEGENT_PLUGIN_AUTH_PORT"), 10, 16) + if err != nil || port == 0 { + return nil, ErrProtocol + } + nonce := make([]byte, 32) + if _, err := io.ReadFull(os.Stdin, nonce); err != nil { + return nil, ErrProtocol + } + conn, err := net.DialTimeout("tcp4", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port))), 5*time.Second) + if err != nil { + return nil, ErrProtocol + } + if err := conn.SetDeadline(time.Now().Add(5 * time.Minute)); err != nil { + conn.Close() + return nil, ErrProtocol + } + if err := writeAll(conn, nonce); err != nil { + conn.Close() + return nil, ErrProtocol + } + return &Channel{conn, connector, credentialType}, nil +} + +func (c *Channel) Receive() (Credential, error) { + var length uint32 + if binary.Read(c.Conn, binary.BigEndian, &length) != nil || length == 0 || length > MaxFrameBytes { + return nil, ErrProtocol + } + data := make([]byte, length) + if _, err := io.ReadFull(c.Conn, data); err != nil { + return nil, ErrProtocol + } + value, err := strictJSON(data) + frame, ok := value.(map[string]any) + if err != nil || !ok || len(frame) != 4 || frame["protocolVersion"] != json.Number("1") || frame["connectorSlug"] != c.Connector || frame["credentialType"] != c.CredentialType { + return nil, ErrProtocol + } + credential, ok := frame["credential"].(map[string]any) + if !ok || validate(Credential(credential), c.CredentialType) != nil { + return nil, ErrProtocol + } + return Credential(credential), nil +} + +func (c *Channel) Send(credential Credential) error { + if err := validate(credential, c.CredentialType); err != nil { + return err + } + data, err := json.Marshal(map[string]any{"protocolVersion": 1, "connectorSlug": c.Connector, "credentialType": c.CredentialType, "credential": credential}) + if err != nil || len(data) == 0 || len(data) > MaxFrameBytes { + return ErrProtocol + } + var prefix [4]byte + binary.BigEndian.PutUint32(prefix[:], uint32(len(data))) + if writeAll(c.Conn, prefix[:]) != nil || writeAll(c.Conn, data) != nil { + return ErrProtocol + } + return nil +} + +func writeAll(writer io.Writer, data []byte) error { + for len(data) > 0 { + n, err := writer.Write(data) + if err != nil { + return err + } + if n <= 0 { + return io.ErrShortWrite + } + data = data[n:] + } + return nil +} + +func strictJSON(data []byte) (any, error) { + if !utf8.Valid(data) { + return nil, ErrProtocol + } + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + value, err := decodeValue(decoder, 0) + if err != nil { + return nil, ErrProtocol + } + if _, err := decoder.Token(); err != io.EOF { + return nil, ErrProtocol + } + return value, nil +} + +func decodeValue(decoder *json.Decoder, depth int) (any, error) { + if depth > 64 { + return nil, ErrProtocol + } + token, err := decoder.Token() + if err != nil { + return nil, err + } + switch token { + case json.Delim('{'): + object := make(map[string]any) + for decoder.More() { + keyToken, err := decoder.Token() + key, ok := keyToken.(string) + if err != nil || !ok { + return nil, ErrProtocol + } + if _, exists := object[key]; exists { + return nil, ErrProtocol + } + value, err := decodeValue(decoder, depth+1) + if err != nil { + return nil, err + } + object[key] = value + } + if end, err := decoder.Token(); err != nil || end != json.Delim('}') { + return nil, ErrProtocol + } + return object, nil + case json.Delim('['): + array := make([]any, 0) + for decoder.More() { + value, err := decodeValue(decoder, depth+1) + if err != nil { + return nil, err + } + array = append(array, value) + } + if end, err := decoder.Token(); err != nil || end != json.Delim(']') { + return nil, ErrProtocol + } + return array, nil + default: + if number, ok := token.(json.Number); ok { + value, err := number.Float64() + if err != nil || math.IsNaN(value) || math.IsInf(value, 0) { + return nil, ErrProtocol + } + } + if _, delimiter := token.(json.Delim); delimiter { + return nil, ErrProtocol + } + return token, nil + } +} diff --git a/plugins/lark/.wework-build/plugin-auth-go/transport_test.go b/plugins/lark/.wework-build/plugin-auth-go/transport_test.go new file mode 100644 index 0000000..e758b30 --- /dev/null +++ b/plugins/lark/.wework-build/plugin-auth-go/transport_test.go @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +package pluginauth + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "net" + "os" + "os/exec" + "strings" + "testing" + "time" +) + +func TestStrictJSONRejectsConfusedFrames(t *testing.T) { + for _, input := range []string{`{"a":1,"a":2}`, `{"a":{"b":1,"b":2}}`, `{"a":NaN}`, `{} {}`, `{"a":[1,]}`, `{"a":1e999}`, string([]byte{'"', 255, '"'})} { + if _, err := strictJSON([]byte(input)); err == nil { + t.Fatal("accepted invalid JSON") + } + } +} + +func TestFrameRejectsWrongIdentityAndOversize(t *testing.T) { + for _, body := range []string{ + `{"protocolVersion":true,"connectorSlug":"test","credentialType":"oauth2","credential":{"access_token":"s"}}`, + `{"protocolVersion":1,"connectorSlug":"other","credentialType":"oauth2","credential":{"access_token":"s"}}`, + `{"protocolVersion":1,"connectorSlug":"test","credentialType":"oauth2","credential":{"access_token":""}}`, + strings.Repeat("x", MaxFrameBytes+1), + } { + left, right := net.Pipe() + done := make(chan struct{}) + go func() { + defer close(done) + defer right.Close() + binary.Write(right, binary.BigEndian, uint32(len(body))) + right.Write([]byte(body)) + }() + channel := Channel{left, "test", "oauth2"} + if _, err := channel.Receive(); err == nil { + t.Fatal("accepted confused frame") + } + left.Close() + <-done + } +} + +func TestNativeProcessRoundTrip(t *testing.T) { + for _, mode := range []string{"export", "refresh", "run", "revoke", "detach", "source_changed"} { + t.Run(mode, func(t *testing.T) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer listener.Close() + listener.(*net.TCPListener).SetDeadline(time.Now().Add(10 * time.Second)) + cmd := exec.Command(os.Args[0], "-test.run=^TestChildProvider$") + nonce := bytes.Repeat([]byte{7}, 32) + cmd.Stdin = bytes.NewReader(nonce) + cmd.Env = append(os.Environ(), "WEGENT_PLUGIN_AUTH_FD=", "WEGENT_PLUGIN_AUTH_PORT="+fmt.Sprint(listener.Addr().(*net.TCPAddr).Port), "WEGENT_SDK_CHILD="+mode) + var output, diagnostic bytes.Buffer + cmd.Stdout, cmd.Stderr = &output, &diagnostic + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer cmd.Process.Kill() + socket, err := listener.Accept() + if err != nil { + t.Fatal(err) + } + defer socket.Close() + socket.SetDeadline(time.Now().Add(10 * time.Second)) + received := make([]byte, 32) + if _, err := io.ReadFull(socket, received); err != nil || !bytes.Equal(received, nonce) { + t.Fatal("capability handshake failed") + } + channel := Channel{socket, "test", "oauth2"} + if mode != "export" { + credential := Credential{"access_token": "synthetic-old-access"} + if mode != "run" { + credential["refresh_token"] = "synthetic-refresh" + } + if err := channel.Send(credential); err != nil { + t.Fatal(err) + } + socket.(*net.TCPConn).CloseWrite() + } + if mode == "export" || mode == "refresh" { + credential, err := channel.Receive() + if err != nil || credential["access_token"] != "synthetic-new-access" { + t.Fatal("native credential response failed") + } + } + if err := cmd.Wait(); err != nil { + t.Fatal("child failed", diagnostic.String()) + } + if strings.Contains(output.String()+diagnostic.String(), "synthetic-") { + t.Fatal("credential escaped private channel") + } + var metadata map[string]any + if json.Unmarshal(output.Bytes(), &metadata) != nil { + t.Fatal("missing public result") + } + if mode == "run" { + if metadata["account"] != "alice" { + t.Fatal("wrong business account") + } + } else if mode == "source_changed" { + if metadata["status"] != "source_changed" { + t.Fatal("missing source fence confirmation") + } + } else if mode == "detach" && metadata["status"] != "detached" { + t.Fatal("missing durable detach confirmation") + } else if mode != "detach" && metadata["status"] != "ok" { + t.Fatal("missing success") + } + }) + } +} + +func TestChildProvider(t *testing.T) { + mode := os.Getenv("WEGENT_SDK_CHILD") + if mode == "" { + return + } + account := func(context.Context) (Account, error) { + fmt.Fprintln(os.Stdout, "synthetic-export-noise") + fmt.Fprintln(os.Stderr, "synthetic-error-noise") + return Account{"alice", Credential{"access_token": "synthetic-new-access", "refresh_token": "synthetic-refresh"}}, nil + } + provider := Provider{Export: account, + Refresh: func(ctx context.Context, value Credential) (Account, error) { + if value["refresh_token"] != "synthetic-refresh" { + return Account{}, ErrProvider + } + return account(ctx) + }, + Revoke: func(context.Context, Credential) error { return nil }, + Detach: func(_ context.Context, id string, value Credential) error { + if id != strings.Repeat("a", 64) || value["refresh_token"] != "synthetic-refresh" { + return ErrProvider + } + fmt.Fprintln(os.Stdout, "synthetic-detach-noise") + if mode == "source_changed" { + return ErrSourceChanged + } + return nil + }, + Allowed: func(args []string) bool { return len(args) == 1 && args[0] == "read" }, + Run: func(ctx context.Context, value Credential, args []string) error { + if _, ok := value["refresh_token"]; ok { + return ErrProvider + } + fmt.Fprintln(os.Stdout, `{"account":"alice"}`) + return nil + }, + } + args := []string{mode} + if mode == "run" { + args = append(args, "read") + } + if mode == "source_changed" { + args = []string{"detach"} + } + if mode == "detach" || mode == "source_changed" { + args = append(args, strings.Repeat("a", 64)) + } + if Serve(context.Background(), "test", "oauth2", provider, args) != nil { + os.Exit(1) + } + os.Exit(0) +} diff --git a/plugins/lark/README.md b/plugins/lark/README.md index b07a586..f5af9c6 100644 --- a/plugins/lark/README.md +++ b/plugins/lark/README.md @@ -1,42 +1,30 @@ -# 飞书插件 - -该插件将 `lark@1.0.3` 的 26 个 Skills 完整适配为 Wegent/WeWork 可发布的 -Codex 插件。Skills 与官方 CLI `1.0.68` 保持版本配套。 - -## 运行与授权 - -- 插件没有 Wegent Backend Connector,也不会把 App Secret、用户 Access Token - 或 Refresh Token 上传到服务端。 -- PATH 中没有配套的 `lark-cli 1.0.68` 时,安装器下载官方平台二进制, - 校验 SHA-256 后安装到当前用户目录: - - macOS/Linux:`~/.wegent-executor/tools/lark-cli/1.0.68//lark-cli` - - Windows:`%LOCALAPPDATA%\Wegent\tools\lark-cli\1.0.68\win32-x64\lark-cli.exe` -- 首次使用运行 `scripts/ensure-lark-ready.*`: - 1. `lark-cli config init --new --brand feishu --lang zh` 显示飞书二维码和链接, - 用户在浏览器完成应用创建或绑定。 - 2. `lark-cli auth login --recommend` 启动 Device Flow,用户在浏览器完成用户授权。 -- 官方 CLI 将非敏感配置保存为权限 `0600` 的 `~/.lark-cli/config.json`。 - macOS/Windows 使用系统钥匙串保存 App Secret 和用户 Token;Linux 使用 - `~/.local/share/lark-cli` 下权限 `0600` 的 AES-GCM 加密文件。 -- 后续调用统一通过 `scripts/run-lark-cli.*`,包装器忽略外部 Connector - 注入的 Token、Brand 和 App ID 环境变量,只使用本机官方 CLI 配置。 -- Windows PowerShell 5.1 下,安装、授权和业务调用均通过原生命令兼容边界, - CLI 写入 stderr 的二维码/OAuth 进度不会绕过退出码处理。 -- 需要额外业务权限时,按照 `lark-shared` Skill 的最小权限规则增量 OAuth。 - 高风险写操作仍由 CLI 的确认门禁保护,禁止自动追加 `--yes`。 - -所有 Shell/PowerShell 包装脚本都按普通文件打包并由解释器调用,不依赖 ZIP -保留可执行位。插件包不内置平台二进制或用户凭据。 - -Windows 可运行 `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/test-native-command.ps1` -验证非零退出码与正常 stderr 的处理。 - -## 能力 - -包含 26 个 Skills,覆盖审批、考勤、多维表格、日历、通讯录、云文档、云空间、 -实时事件、即时通讯、邮箱、Markdown、妙记、会议纪要、OKR、电子表格、幻灯片、 -任务、视频会议、画板、知识库以及会议纪要和开工摘要工作流。 - -Skills 和 CLI 来自官方 -[larksuite/cli](https://github.com/larksuite/cli),使用 MIT 许可证;原始许可证 -保留在插件根目录。 +# 飞书 / Lark + +通过原生 Lark CLI 使用文档、消息、日历、审批等能力,支持 Wegent 本地登录与云端托管认证。 + +## 连接与身份 + +- **飞书用户(lark)**:在来源设备完成原生浏览器授权。Wegent 接管 OAuth 刷新权后,本地和云端调用都使用托管连接,不需要在云端再次扫码。交接只删除所选账户的本地用户令牌,不调用会撤销服务端授权的 CLI logout。 +- **飞书应用(lark-app)**:单独同步 App ID 和 App Secret,用于 `--as bot`。适配器在私有进程中换取应用令牌,原 CLI 业务命令仅接收令牌。用户连接不会自动授予应用身份权限。 + +云端默认使用用户身份;应用调用必须显式指定 `--as bot` 并授权应用连接。本地保留配置的默认身份。品牌由本地配置决定,支持 Feishu 和 Lark。 + +始终通过 `scripts/run-lark-cli.sh` 或 Windows 的 `scripts/run-lark-cli.ps1` 调用业务命令。托管模式禁止 CLI 自行登录、修改配置或切换 profile。需要新增 scope 时,在来源设备通过 wrapper 发起 `auth login --scope ...` 或 `--domain ...`,再在 Wegent 原生连接界面重新连接。云端遇到权限不足时返回来源设备处理。写操作保留原 CLI 的确认规则。 + +## 认证边界 + +用户刷新令牌与应用密钥仅进入私有适配器,不通过命令行、环境变量或业务输出传递。托管业务命令使用内存凭据提供器,禁用本地 keychain、用户插件与配置覆盖;携带凭据的 HTTP 请求只允许对应品牌的官方 HTTPS API。 + +交接通过来源锁、凭据指纹和持久化回执支持恢复,凭据发生变化时停止交接。独立运行的裸 `lark-cli` 不遵守 Wegent 来源锁:交接期间请关闭它;交接后如需独立使用,应采用独立配置与授权,避免共用同一 OAuth grant。 + +## 构建与验证 + +安装包自带基于官方 CLI **v1.0.68** 构建的原生适配器,支持 macOS amd64/arm64、Linux amd64/arm64、Windows amd64。上游源码与构建产物均校验 SHA-256,二进制以 XZ 存储,运行时校验后解压到宿主执行目录。运行需要 Python 3.9+;源码目录必须先执行插件构建。 + +```sh +uv run --no-project python .wework-build/lark-auth/package.py --plugin . --output build/lark-package.zip +``` + +`.wework-build.json` 声明完整构建入口,插件目录包含所需 SDK、构建与验证脚本。CI 在三个操作系统上构建五个平台产物并验证打包后的认证路由、私有通道及错误退出码;测试使用合成凭据,不接触真实账户。真实账号授权与云端运行仍需在配套的 Wegent Backend、Executor 已部署后验收。 + +上游许可见安装包 `scripts/native//UPSTREAM-LICENSE`,适配器 SDK 许可见 `.wework-build/plugin-auth-go/`。 diff --git a/plugins/lark/scripts/account-auth-bot.py b/plugins/lark/scripts/account-auth-bot.py new file mode 100644 index 0000000..fdfa850 --- /dev/null +++ b/plugins/lark/scripts/account-auth-bot.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import json +import sys +from native_runtime import adapter_main + +if __name__ == "__main__": + try: + raise SystemExit(adapter_main("bot", sys.argv[1:])) + except Exception: + print(json.dumps({"error": "plugin_auth_lark_failed"})) + raise SystemExit(1) diff --git a/plugins/lark/scripts/account-auth.py b/plugins/lark/scripts/account-auth.py new file mode 100644 index 0000000..cf51337 --- /dev/null +++ b/plugins/lark/scripts/account-auth.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import json +import sys +from native_runtime import adapter_main + +if __name__ == "__main__": + try: + raise SystemExit(adapter_main("user", sys.argv[1:])) + except Exception: + print(json.dumps({"error": "plugin_auth_lark_failed"})) + raise SystemExit(1) diff --git a/plugins/lark/scripts/ensure-lark-ready.ps1 b/plugins/lark/scripts/ensure-lark-ready.ps1 index 7c6efa6..33359f6 100644 --- a/plugins/lark/scripts/ensure-lark-ready.ps1 +++ b/plugins/lark/scripts/ensure-lark-ready.ps1 @@ -1,57 +1,9 @@ +param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest - -$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path -. (Join-Path $scriptDirectory 'invoke-native-command.ps1') -$lark = & (Join-Path $scriptDirectory 'install-lark-cli.ps1') | - Select-Object -Last 1 - -foreach ($name in @( - 'LARKSUITE_CLI_USER_ACCESS_TOKEN', - 'LARKSUITE_CLI_BRAND', - 'LARKSUITE_CLI_APP_ID' -)) { - Remove-Item "Env:$name" -ErrorAction SilentlyContinue -} -$env:LARKSUITE_CLI_NO_UPDATE_NOTIFIER = '1' -$env:LARKSUITE_CLI_NO_SKILLS_NOTIFIER = '1' - -function Test-LarkUserAuth { - $statusExitCode = -1 - $status = Invoke-NativeCommand ` - -Command { & $lark auth status --json 2>$null } ` - -ExitCode ([ref]$statusExitCode) | Out-String - return $statusExitCode -eq 0 -and - $status -match '"identity"\s*:\s*"user"' -} - -$configExitCode = -1 -Invoke-NativeCommand ` - -Command { & $lark config show } ` - -ExitCode ([ref]$configExitCode) ` - -DiscardOutput -if ($configExitCode -ne 0) { - Write-Host 'Lark CLI is not configured. Complete the Feishu QR setup shown below.' - Invoke-NativeCommand ` - -Command { & $lark config init --new --brand feishu --lang zh } ` - -ExitCode ([ref]$configExitCode) - if ($configExitCode -ne 0) { - throw 'Lark CLI configuration was not completed.' - } -} - -if (-not (Test-LarkUserAuth)) { - Write-Host 'Lark CLI has no local user authorization. Complete the browser authorization below.' - $loginExitCode = -1 - Invoke-NativeCommand ` - -Command { & $lark auth login --recommend } ` - -ExitCode ([ref]$loginExitCode) - if ($loginExitCode -ne 0) { - throw 'Lark user authorization failed.' - } -} - -if (-not (Test-LarkUserAuth)) { - throw 'Lark authorization did not produce a usable local user identity.' -} -Write-Host 'Lark CLI is installed, configured and locally authorized.' +. (Join-Path $PSScriptRoot 'invoke-native-command.ps1') +$result = -1 +Invoke-NativeCommand -Command { + & (Join-Path $PSScriptRoot 'run-python.ps1') (Join-Path $PSScriptRoot 'lark_cli.py') '--ready' +} -ExitCode ([ref]$result) +exit $result diff --git a/plugins/lark/scripts/ensure-lark-ready.sh b/plugins/lark/scripts/ensure-lark-ready.sh index 7853a6a..4610ade 100644 --- a/plugins/lark/scripts/ensure-lark-ready.sh +++ b/plugins/lark/scripts/ensure-lark-ready.sh @@ -1,34 +1,4 @@ #!/bin/sh - set -eu - -LARK_SCRIPT_DIRECTORY="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -LARK_EXECUTABLE="$(/bin/sh "${LARK_SCRIPT_DIRECTORY}/install-lark-cli.sh")" - -unset LARKSUITE_CLI_USER_ACCESS_TOKEN LARKSUITE_CLI_BRAND LARKSUITE_CLI_APP_ID -export LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1 -export LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1 - -has_local_user_auth() { - LARK_AUTH_STATUS="$("${LARK_EXECUTABLE}" auth status --json 2>/dev/null)" || - return 1 - printf '%s\n' "${LARK_AUTH_STATUS}" | - grep -E '"identity"[[:space:]]*:[[:space:]]*"user"' >/dev/null -} - -if ! "${LARK_EXECUTABLE}" config show >/dev/null 2>&1; then - echo "Lark CLI is not configured. Complete the Feishu QR setup shown below." >&2 - "${LARK_EXECUTABLE}" config init --new --brand feishu --lang zh -fi - -if ! has_local_user_auth; then - echo "Lark CLI has no local user authorization. Complete the browser authorization below." >&2 - "${LARK_EXECUTABLE}" auth login --recommend -fi - -if ! has_local_user_auth; then - echo "Lark authorization did not produce a usable local user identity." >&2 - exit 20 -fi - -echo "Lark CLI is installed, configured and locally authorized." +SCRIPT_DIRECTORY="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +exec sh "${SCRIPT_DIRECTORY}/run-python.sh" "${SCRIPT_DIRECTORY}/lark_cli.py" --ready "$@" diff --git a/plugins/lark/scripts/lark_cli.py b/plugins/lark/scripts/lark_cli.py new file mode 100644 index 0000000..bed7c8a --- /dev/null +++ b/plugins/lark/scripts/lark_cli.py @@ -0,0 +1,113 @@ +"""Route public commands before any local authentication or installation.""" + +import json +import os +import sys +import re +from pathlib import Path + +import native_runtime as native +from wegent_plugin_auth import AuthError, run_account_command + +ROOT = Path(__file__).resolve().parents[1] + + +def validate_scopes(arguments): + if ( + not isinstance(arguments, list) + or not arguments + or len(arguments) > 8 + or len(arguments) % 2 + ): + raise AuthError("plugin_auth_invalid_command") + for flag, value in zip(arguments[::2], arguments[1::2]): + if ( + flag not in ("--domain", "--scope") + or not isinstance(value, str) + or not re.fullmatch(r"[A-Za-z0-9_:,. /-]{1,4096}", value) + ): + raise AuthError("plugin_auth_invalid_command") + + +def scope_request(arguments, bot_account): + validate_scopes(arguments) + if not bot_account: + raise AuthError("plugin_auth_invalid_command") + path = native.receipt(bot_account).with_suffix(".scopes.json") + temporary = path.with_suffix(".tmp") + temporary.write_text(json.dumps(arguments)) + temporary.replace(path) + + +def identity(arguments): + result = "user" + for index, argument in enumerate(arguments): + if argument == "--as": + if index + 1 >= len(arguments): + raise AuthError("plugin_auth_invalid_command") + result = arguments[index + 1] + elif argument.startswith("--as="): + result = argument.split("=", 1)[1] + if result not in ("user", "bot", "auto"): + raise AuthError("plugin_auth_invalid_command") + return "user" if result == "auto" else result + + +def business_args(arguments): + if arguments == ["--ready"] or arguments[:2] == ["auth", "status"]: + return ["account-status"] + if not arguments or arguments[0] in { + "auth", + "config", + "profile", + "init", + "update", + "skills", + "plugin", + }: + raise AuthError("plugin_auth_use_native_login") + return arguments + + +def main(arguments): + who = identity(arguments) + slug = "lark-app" if who == "bot" else "lark" + if os.environ.get("WEGENT_PLUGIN_AUTH_MODE") == "cloud": + sys.stdout.write(run_account_command(ROOT, slug, business_args(arguments))) + return 0 + with native.locked(): + value = native.context() + if ( + who == "user" + and not any(arg.startswith("--as") for arg in arguments) + and value.get("default_as") == "bot" + ): + who, slug = "bot", "lark-app" + account = value.get(who, "") + managed = account and native.receipt(account).exists() + if not managed: + if arguments == ["--ready"]: + from local_auth import login_locked + + login_locked() + print(json.dumps({"status": "ok"})) + return 0 + return native.invoke(["local", *arguments]).returncode + if arguments[:2] == ["auth", "login"]: + scope_request(arguments[2:], value.get("bot", "")) + raise AuthError("plugin_auth_reconnect_required") + sys.stdout.write( + run_account_command(ROOT, slug, business_args(arguments), account_id=account) + ) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main(sys.argv[1:])) + except AuthError as error: + print(json.dumps({"error": str(error)})) + raise SystemExit(1) + except Exception: + print(json.dumps({"error": "plugin_auth_lark_failed"})) + raise SystemExit(1) diff --git a/plugins/lark/scripts/local-auth.ps1 b/plugins/lark/scripts/local-auth.ps1 new file mode 100644 index 0000000..cb02472 --- /dev/null +++ b/plugins/lark/scripts/local-auth.ps1 @@ -0,0 +1,9 @@ +param([ValidateSet('health', 'login', 'logout')][string]$Action = 'health') +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +. (Join-Path $PSScriptRoot 'invoke-native-command.ps1') +$result = -1 +Invoke-NativeCommand -Command { + & (Join-Path $PSScriptRoot 'run-python.ps1') (Join-Path $PSScriptRoot 'local_auth.py') $Action +} -ExitCode ([ref]$result) +exit $result diff --git a/plugins/lark/scripts/local-auth.sh b/plugins/lark/scripts/local-auth.sh new file mode 100644 index 0000000..e285d15 --- /dev/null +++ b/plugins/lark/scripts/local-auth.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu +SCRIPT_DIRECTORY="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +exec sh "${SCRIPT_DIRECTORY}/run-python.sh" "${SCRIPT_DIRECTORY}/local_auth.py" "$@" diff --git a/plugins/lark/scripts/local_auth.py b/plugins/lark/scripts/local_auth.py new file mode 100644 index 0000000..9866b86 --- /dev/null +++ b/plugins/lark/scripts/local_auth.py @@ -0,0 +1,142 @@ +"""Original device flows, with verification links opened on the source device.""" + +import json +import queue +import re +import subprocess +import sys +import threading +import time +import urllib.parse +import webbrowser +from pathlib import Path + +import native_runtime as native +from wegent_plugin_auth import AuthError, run_account_command + +ROOT = Path(__file__).resolve().parents[1] + + +def verification_urls(line): + for value in re.findall(r'https://[^\s<>"\x1b]+', line): + parsed = urllib.parse.urlsplit(value) + if ( + parsed.hostname in {"accounts.feishu.cn", "accounts.larksuite.com"} + and parsed.port in (None, 443) + and not parsed.username + and not parsed.password + ): + yield value + + +def browser_flow(arguments): + process = subprocess.Popen( + [str(native.companion()), "local", *arguments], + env=native.environment(), + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + ) + events = queue.Queue() + + def consume(): + for line in process.stdout: + events.put(line) + events.put(None) + + reader = threading.Thread(target=consume, daemon=True) + reader.start() + opened = set() + deadline = time.monotonic() + 240 + try: + while time.monotonic() < deadline: + try: + line = events.get(timeout=0.2) + except queue.Empty: + continue + if line is None: + if process.wait(timeout=5) != 0: + raise AuthError("plugin_auth_login_failed") + return + for url in verification_urls(line): + if url not in opened: + opened.add(url) + if not webbrowser.open(url): + raise AuthError("plugin_auth_browser_unavailable") + raise AuthError("plugin_auth_login_timeout") + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=5) + process.stdout.close() + + +def login_locked(): + if not native.context(): + browser_flow(["config", "init", "--new", "--brand", "feishu", "--lang", "zh"]) + value = native.context() + request = native.receipt(value.get("bot", "")).with_suffix(".scopes.json") + arguments = ["--recommend"] + if request.exists(): + if request.stat().st_size > 40000: + raise AuthError("plugin_auth_invalid_command") + arguments = json.loads(request.read_text()) + from lark_cli import validate_scopes + + validate_scopes(arguments) + if request.exists() or native.invoke(["local-health"], capture=True).returncode: + browser_flow(["auth", "login", *arguments, "--json"]) + if native.invoke(["local-health"], capture=True).returncode: + raise AuthError("plugin_auth_login_failed") + # Clear routing only after the new grant exists; the host owns reconnect intent. + value = native.context() + if value.get("user"): + native.receipt(value["user"]).unlink(missing_ok=True) + request.unlink(missing_ok=True) + + +def action(command): + with native.locked(): + value = native.context() + account = value.get("user", "") + managed = account and native.receipt(account).exists() + if command == "login": + login_locked() + return "ok" + if command == "logout": + if value and native.invoke(["local-clear"], capture=True).returncode: + raise AuthError("plugin_auth_logout_failed") + if account: + native.receipt(account).unlink(missing_ok=True) + return "ok" + if command != "health": + raise AuthError("plugin_auth_invalid_command") + if not managed: + return ( + "ok" + if native.invoke(["local-health"], capture=True).returncode == 0 + else "need_login" + ) + try: + value = json.loads( + run_account_command(ROOT, "lark", ["account-status"], account_id=account) + ) + return "ok" if value.get("status") == "ok" else "need_login" + except (AuthError, ValueError): + return "need_login" + + +if __name__ == "__main__": + try: + if len(sys.argv) != 2: + raise AuthError("plugin_auth_invalid_command") + print(json.dumps({"status": action(sys.argv[1])})) + except Exception: + print( + json.dumps( + {"status": "error", "hint": "Lark authentication did not complete."} + ) + ) diff --git a/plugins/lark/scripts/native_runtime.py b/plugins/lark/scripts/native_runtime.py new file mode 100644 index 0000000..cddc635 --- /dev/null +++ b/plugins/lark/scripts/native_runtime.py @@ -0,0 +1,210 @@ +"""Verified native package loading and serialized access to the upstream store.""" + +import contextlib +import hashlib +import json +import lzma +import os +import platform +import subprocess +import tempfile +import time +from pathlib import Path + +from wegent_plugin_auth import AuthError, local_configuration + +ROOT = Path(__file__).resolve().parent + + +def state_root(): + path = Path.home() / ".wegent-executor/plugin-auth/lark" + path.mkdir(parents=True, exist_ok=True, mode=0o700) + return path + + +@contextlib.contextmanager +def locked(): + # All profiles share one OS keychain namespace, including custom config dirs. + with (state_root() / "source.lock").open("a+b") as stream: + if stream.tell() == 0: + stream.write(b"0") + stream.flush() + stream.seek(0) + deadline = time.monotonic() + 30 + while True: + try: + if os.name == "nt": + import msvcrt + + msvcrt.locking(stream.fileno(), msvcrt.LK_NBLCK, 1) + else: + import fcntl + + fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB) + break + except OSError: + if time.monotonic() >= deadline: + raise AuthError("plugin_auth_source_busy") from None + time.sleep(0.05) + try: + yield + finally: + stream.seek(0) + if os.name == "nt": + msvcrt.locking(stream.fileno(), msvcrt.LK_UNLCK, 1) + else: + fcntl.flock(stream, fcntl.LOCK_UN) + + +def environment(source=True): + allowed = { + "HOME", + "USERPROFILE", + "PATH", + "SYSTEMROOT", + "WINDIR", + "APPDATA", + "LOCALAPPDATA", + "TEMP", + "TMP", + "LANG", + "XDG_DATA_HOME", + "WEGENT_EXECUTOR_HOME", + "WEGENT_PLUGIN_AUTH_PORT", + "LARKSUITE_CLI_CONFIG_DIR", + } + result = {key: value for key, value in os.environ.items() if key in allowed} + settings = local_configuration() + if set(settings) - {"LARKSUITE_CLI_CONFIG_DIR", "XDG_DATA_HOME"}: + raise AuthError("plugin_auth_invalid_local_configuration") + result.update(settings) + result.update( + LARKSUITE_CLI_NO_UPDATE_NOTIFIER="1", LARKSUITE_CLI_NO_SKILLS_NOTIFIER="1" + ) + if source: + result["WEGENT_LARK_STATE_DIR"] = str(state_root()) + else: + result.pop("LARKSUITE_CLI_CONFIG_DIR", None) + result.pop("XDG_DATA_HOME", None) + return result + + +def companion(): + system = {"Darwin": "darwin", "Linux": "linux", "Windows": "windows"}.get( + platform.system() + ) + arch = { + "arm64": "arm64", + "aarch64": "arm64", + "x86_64": "amd64", + "AMD64": "amd64", + }.get(platform.machine()) + folder = ROOT / "native" / f"{system}-{arch}" + blob, metadata = folder / "lark-account-auth.xz", folder / "lark-account-auth.json" + if blob.is_symlink() or metadata.is_symlink() or metadata.stat().st_size > 65536: + raise AuthError("plugin_auth_package_sync_required") + value = json.loads(metadata.read_text(encoding="utf-8")) + if ( + value.get("nativeProtocolVersion") != 1 + or value.get("target") != f"{system}/{arch}" + or not 0 < value.get("binaryBytes", 0) < 80 * 1024 * 1024 + ): + raise AuthError("plugin_auth_package_sync_required") + packed = blob.read_bytes() + if hashlib.sha256(packed).hexdigest() != value["compressedSha256"]: + raise AuthError("plugin_auth_package_sync_required") + cache = ( + Path( + os.environ.get( + "WEGENT_EXECUTOR_HOME", str(Path.home() / ".wegent-executor") + ) + ) + / "plugin-native/lark" + ) + cache.mkdir(parents=True, exist_ok=True, mode=0o700) + digest = value["binarySha256"] + if len(digest) != 64 or any(c not in "0123456789abcdef" for c in digest): + raise AuthError("plugin_auth_package_sync_required") + executable = cache / (digest + (".exe" if system == "windows" else "")) + if executable.is_symlink(): + raise AuthError("plugin_auth_package_sync_required") + if ( + executable.exists() + and hashlib.sha256(executable.read_bytes()).hexdigest() == digest + ): + return executable + decoder = lzma.LZMADecompressor(memlimit=128 * 1024 * 1024) + content = decoder.decompress(packed, max_length=value["binaryBytes"] + 1) + if ( + not decoder.eof + or len(content) != value["binaryBytes"] + or hashlib.sha256(content).hexdigest() != digest + ): + raise AuthError("plugin_auth_package_sync_required") + fd, temporary = tempfile.mkstemp(dir=cache) + try: + with os.fdopen(fd, "wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.chmod(temporary, 0o700) + os.replace(temporary, executable) + finally: + Path(temporary).unlink(missing_ok=True) + return executable + + +def invoke(arguments, *, capture=False, timeout=240): + source = not ( + len(arguments) > 1 + and arguments[0] in ("user", "bot") + and arguments[1] in ("run", "refresh", "revoke") + ) + return subprocess.run( + [str(companion()), *arguments], + env=environment(source=source), + capture_output=capture, + timeout=timeout, + check=False, + ) + + +def receipt(account): + name = "account-" + hashlib.sha256(account.encode()).hexdigest() + ".json" + return state_root() / name + + +def context(): + result = invoke(["context"], capture=True) + if result.returncode or len(result.stdout) > 16384: + return {} + return json.loads(result.stdout) + + +def require_idle_source(): + command = ( + ["tasklist", "/FI", "IMAGENAME eq lark-cli.exe", "/FO", "CSV", "/NH"] + if os.name == "nt" + else ["pgrep", "-x", "lark-cli"] + ) + result = subprocess.run(command, capture_output=True, timeout=10) + if os.name == "nt": + import csv + + busy = any( + row and row[0].lower() == "lark-cli.exe" + for row in csv.reader(result.stdout.decode(errors="replace").splitlines()) + ) + failed = result.returncode != 0 + else: + busy, failed = result.returncode == 0, result.returncode not in (0, 1) + if busy or failed: + raise AuthError("plugin_auth_source_busy") + + +def adapter_main(identity, arguments): + if arguments and arguments[0] in ("export", "detach"): + with locked(): + require_idle_source() + return invoke([identity, *arguments]).returncode + return invoke([identity, *arguments]).returncode diff --git a/plugins/lark/scripts/run-lark-cli.ps1 b/plugins/lark/scripts/run-lark-cli.ps1 index c74959e..ba4ac56 100644 --- a/plugins/lark/scripts/run-lark-cli.ps1 +++ b/plugins/lark/scripts/run-lark-cli.ps1 @@ -1,26 +1,9 @@ -param( - [Parameter(ValueFromRemainingArguments = $true)] - [string[]]$LarkArguments -) - +param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest - -$scriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path -. (Join-Path $scriptDirectory 'invoke-native-command.ps1') -$lark = & (Join-Path $scriptDirectory 'install-lark-cli.ps1') | - Select-Object -Last 1 -foreach ($name in @( - 'LARKSUITE_CLI_USER_ACCESS_TOKEN', - 'LARKSUITE_CLI_BRAND', - 'LARKSUITE_CLI_APP_ID' -)) { - Remove-Item "Env:$name" -ErrorAction SilentlyContinue -} -$env:LARKSUITE_CLI_NO_UPDATE_NOTIFIER = '1' -$env:LARKSUITE_CLI_NO_SKILLS_NOTIFIER = '1' -$larkExitCode = -1 -Invoke-NativeCommand ` - -Command { & $lark @LarkArguments } ` - -ExitCode ([ref]$larkExitCode) -exit $larkExitCode +. (Join-Path $PSScriptRoot 'invoke-native-command.ps1') +$result = -1 +Invoke-NativeCommand -Command { + & (Join-Path $PSScriptRoot 'run-python.ps1') (Join-Path $PSScriptRoot 'lark_cli.py') @Arguments +} -ExitCode ([ref]$result) +exit $result diff --git a/plugins/lark/scripts/run-lark-cli.sh b/plugins/lark/scripts/run-lark-cli.sh index d55284c..aec7534 100644 --- a/plugins/lark/scripts/run-lark-cli.sh +++ b/plugins/lark/scripts/run-lark-cli.sh @@ -1,11 +1,4 @@ #!/bin/sh - set -eu - -LARK_SCRIPT_DIRECTORY="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -LARK_EXECUTABLE="$(/bin/sh "${LARK_SCRIPT_DIRECTORY}/install-lark-cli.sh")" - -unset LARKSUITE_CLI_USER_ACCESS_TOKEN LARKSUITE_CLI_BRAND LARKSUITE_CLI_APP_ID -export LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1 -export LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1 -exec "${LARK_EXECUTABLE}" "$@" +SCRIPT_DIRECTORY="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +exec sh "${SCRIPT_DIRECTORY}/run-python.sh" "${SCRIPT_DIRECTORY}/lark_cli.py" "$@" diff --git a/plugins/lark/scripts/run-python.ps1 b/plugins/lark/scripts/run-python.ps1 new file mode 100644 index 0000000..9574ca0 --- /dev/null +++ b/plugins/lark/scripts/run-python.ps1 @@ -0,0 +1,16 @@ +param( + [Parameter(Mandatory = $true)][string]$ScriptPath, + [Parameter(ValueFromRemainingArguments = $true)][string[]]$ScriptArguments +) +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest +. (Join-Path $PSScriptRoot 'invoke-native-command.ps1') +$python = Get-Command python, py, python3 -ErrorAction SilentlyContinue | Select-Object -First 1 +if ($null -eq $python) { throw 'Python 3.9 or newer is required for this plugin.' } +$arguments = @() +if ($python.Name -match '^py(\.exe)?$') { $arguments += '-3' } +$arguments += $ScriptPath +$arguments += $ScriptArguments +$result = -1 +Invoke-NativeCommand -Command { & $python.Source @arguments } -ExitCode ([ref]$result) +exit $result diff --git a/plugins/lark/scripts/run-python.sh b/plugins/lark/scripts/run-python.sh new file mode 100644 index 0000000..9feaba9 --- /dev/null +++ b/plugins/lark/scripts/run-python.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu +if command -v python3 >/dev/null 2>&1; then + exec python3 "$@" +fi +if command -v python >/dev/null 2>&1; then + exec python "$@" +fi +echo 'Python 3.9 or newer is required for this plugin.' >&2 +exit 2 diff --git a/plugins/lark/scripts/tests/test_runtime.py b/plugins/lark/scripts/tests/test_runtime.py new file mode 100644 index 0000000..a09432e --- /dev/null +++ b/plugins/lark/scripts/tests/test_runtime.py @@ -0,0 +1,191 @@ +import contextlib +import http.server +import json +import os +import subprocess +import sys +import tempfile +import threading +import time +import unittest +from pathlib import Path +from unittest.mock import patch + +SCRIPTS = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(SCRIPTS)) +import lark_cli +import local_auth +import native_runtime as native +from wegent_plugin_auth import AuthError + + +class RuntimeTests(unittest.TestCase): + def test_scope_requests_are_bounded_and_validated(self): + lark_cli.validate_scopes(["--scope", "im:message:readonly offline_access"]) + for value in ( + None, + "--scope", + [], + ["--scope"], + ["--scope", 1], + ["--app-secret", "bad"], + ["--scope", "x\n"], + ["--scope", "x"] * 5, + ): + with self.assertRaises(AuthError): + lark_cli.validate_scopes(value) + + def test_identity_routing_is_explicit(self): + self.assertEqual(lark_cli.identity(["im", "--as", "bot"]), "bot") + self.assertEqual(lark_cli.identity(["im", "--as=user"]), "user") + with self.assertRaises(AuthError): + lark_cli.identity(["im", "--as"]) + + def test_managed_commands_cannot_start_a_second_login(self): + for args in (["auth", "login"], ["config", "init"], ["profile", "switch"]): + with self.assertRaises(AuthError): + lark_cli.business_args(args) + self.assertEqual(lark_cli.business_args(["auth", "status"]), ["account-status"]) + + def test_browser_only_opens_official_verification_origins(self): + self.assertEqual( + list( + local_auth.verification_urls( + "https://accounts.feishu.cn/oauth/verify?code=synthetic" + ) + ), + ["https://accounts.feishu.cn/oauth/verify?code=synthetic"], + ) + for value in ( + "https://evil.invalid", + "https://accounts.feishu.cn.evil.invalid", + "https://user@accounts.feishu.cn", + "https://accounts.feishu.cn:444/", + ): + self.assertEqual(list(local_auth.verification_urls(value)), []) + + def test_failed_relogin_preserves_receipt(self): + with tempfile.TemporaryDirectory() as directory: + receipt = Path(directory) / "receipt.json" + receipt.touch() + with patch.object( + native, "context", return_value={"user": "account"} + ), patch.object(native, "receipt", return_value=receipt), patch.object( + native, "invoke", return_value=subprocess.CompletedProcess([], 1) + ), patch.object( + local_auth, "browser_flow", side_effect=AuthError("failed") + ): + with self.assertRaises(AuthError): + local_auth.login_locked() + self.assertTrue(receipt.exists()) + + def test_managed_health_does_not_read_tokens(self): + with tempfile.TemporaryDirectory() as directory: + receipt = Path(directory) / "receipt.json" + receipt.touch() + with patch.object(native, "locked", contextlib.nullcontext), patch.object( + native, "context", return_value={"user": "account"} + ), patch.object(native, "receipt", return_value=receipt), patch.object( + native, "invoke", side_effect=AssertionError("local token probe") + ), patch.object( + local_auth, "run_account_command", return_value='{"status":"ok"}' + ) as run: + self.assertEqual(local_auth.action("health"), "ok") + run.side_effect = AuthError("plugin_auth_device_not_granted") + self.assertEqual(local_auth.action("health"), "need_login") + + def test_cloud_process_only_sends_public_broker_request(self): + requests = [] + + class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_POST(self): + requests.append( + json.loads(self.rfile.read(int(self.headers["Content-Length"]))) + ) + self.send_response(200) + self.end_headers() + self.wfile.write(b'{"stdout":"{\\"status\\":\\"ok\\"}"}') + + with tempfile.TemporaryDirectory() as directory: + home = Path(directory) + capabilities = home / "executor/capabilities" + capabilities.mkdir(parents=True) + (capabilities / "manifest.json").write_text( + json.dumps( + { + "plugins": { + "lark": { + "managed": True, + "enabled": True, + "installed_plugin_id": 123, + "store_path": str(SCRIPTS.parent), + } + } + } + ) + ) + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) + threading.Thread(target=server.serve_forever, daemon=True).start() + env = { + **os.environ, + "HOME": str(home), + "USERPROFILE": str(home), + "WEGENT_EXECUTOR_HOME": str(home / "executor"), + "WEGENT_PLUGIN_AUTH_MODE": "cloud", + "WEGENT_PLUGIN_AUTH_BROKER": f"http://127.0.0.1:{server.server_port}/v1/run", + "WEGENT_PLUGIN_AUTH_BROKER_TOKEN": "a" * 64, + } + try: + for args, slug in [ + (["im", "messages", "list"], "lark"), + (["im", "messages", "list", "--as", "bot"], "lark-app"), + (["--ready"], "lark"), + ]: + result = subprocess.run( + [sys.executable, str(SCRIPTS / "lark_cli.py"), *args], + env=env, + capture_output=True, + timeout=15, + ) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertEqual(requests[-1]["connector_slug"], slug) + self.assertNotIn("credential", requests[-1]) + self.assertFalse((home / ".wegent-executor").exists()) + finally: + server.shutdown() + server.server_close() + + def test_source_lock_blocks_another_real_process(self): + with tempfile.TemporaryDirectory() as directory: + home = Path(directory) + env = {**os.environ, "HOME": str(home), "USERPROFILE": str(home)} + code = f"import sys;sys.path.insert(0,{str(SCRIPTS)!r});import native_runtime as n;\nwith n.locked(): print('locked',flush=True);sys.stdin.read()" + child = subprocess.Popen( + [sys.executable, "-c", code], + env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + ) + try: + self.assertEqual(child.stdout.readline(), b"locked\n") + probe = subprocess.Popen( + [ + sys.executable, + "-c", + f"import sys;sys.path.insert(0,{str(SCRIPTS)!r});import native_runtime as n;\nwith n.locked(): print('entered',flush=True)", + ], + env=env, + stdout=subprocess.PIPE, + ) + time.sleep(0.15) + self.assertIsNone(probe.poll()) + child.communicate(timeout=5) + out, _ = probe.communicate(timeout=5) + self.assertEqual(out, b"entered\n") + finally: + if child.poll() is None: + child.kill() + child.communicate() diff --git a/plugins/lark/scripts/wegent_plugin_auth/LICENSE b/plugins/lark/scripts/wegent_plugin_auth/LICENSE new file mode 100644 index 0000000..b8c67ae --- /dev/null +++ b/plugins/lark/scripts/wegent_plugin_auth/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2025 Weibo, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/plugins/lark/scripts/wegent_plugin_auth/__init__.py b/plugins/lark/scripts/wegent_plugin_auth/__init__.py new file mode 100644 index 0000000..762bf3c --- /dev/null +++ b/plugins/lark/scripts/wegent_plugin_auth/__init__.py @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Dependency-free native plugin credential transport (protocol draft 1).""" + +from .adapter import AccountAuthAdapter, AuthError, SourceChanged +from .configuration import local_configuration +from .runtime import delegate_cloud_command, run_account_command + +__version__ = "0.7.1" +__all__ = [ + "AccountAuthAdapter", + "AuthError", + "SourceChanged", + "local_configuration", + "delegate_cloud_command", + "run_account_command", +] diff --git a/plugins/lark/scripts/wegent_plugin_auth/adapter.py b/plugins/lark/scripts/wegent_plugin_auth/adapter.py new file mode 100644 index 0000000..9813281 --- /dev/null +++ b/plugins/lark/scripts/wegent_plugin_auth/adapter.py @@ -0,0 +1,195 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Plugin callbacks without descriptor, framing, or envelope boilerplate.""" + +from __future__ import annotations + +import contextlib +import json +import os +from collections.abc import Callable, Sequence +from typing import Any, BinaryIO, Literal + +from .runtime import native_execution_scope +from .transport import AuthError, open_pipe, read_frame, write_frame + +Credential = dict[str, Any] +CredentialType = Literal["password", "bearer", "oauth2"] +_REQUIRED_FIELDS = { + "password": ("username", "password"), + "bearer": ("token",), + "oauth2": ("access_token",), +} + + +class SourceChanged(AuthError): + """Detach durably fenced off this transfer ID without deleting the new grant.""" + + +@contextlib.contextmanager +def _quiet_callbacks(): + # Authentication callbacks may accidentally print upstream errors or secrets. + # Business command output remains owned by the plugin's execute callback. + with open(os.devnull, "w") as sink: + with contextlib.redirect_stdout(sink), contextlib.redirect_stderr(sink): + yield + + +class AccountAuthAdapter: + """Use only in a dedicated process launched by an authenticated native broker. + + OAuth tokens can be transported, but refresh ownership remains the host's + responsibility. This SDK does not authorize devices or migrate local storage. + """ + + def __init__( + self, + *, + connector_slug: str, + credential_type: CredentialType, + export: Callable[[], Credential | None], + account_id: Callable[[Credential], str], + execute: Callable[[Credential, Sequence[str]], int], + allowed_commands: Sequence[str], + validate: Callable[[Credential], None] | None = None, + authorize: Callable[[], Credential] | None = None, + refresh: Callable[[Credential], Credential] | None = None, + revoke: Callable[[Credential], None] | None = None, + detach: Callable[[str, Credential], None] | None = None, + ) -> None: + if credential_type not in _REQUIRED_FIELDS or not connector_slug: + raise AuthError("Invalid adapter definition") + self.connector_slug = connector_slug + self.credential_type = credential_type + self._export = export + self._account_id = account_id + self._execute = execute + self._validate = validate + self._allowed_commands = frozenset(allowed_commands) + if credential_type != "oauth2" and any((authorize, refresh, revoke, detach)): + raise AuthError("OAuth callbacks require an OAuth adapter") + self._authorize = authorize + self._refresh = refresh + self._revoke = revoke + self._detach = detach + + def _validate_credential(self, value: Any) -> Credential: + try: + if not isinstance(value, dict): + raise ValueError + for key in _REQUIRED_FIELDS[self.credential_type]: + if not isinstance(value.get(key), str) or not value[key].strip(): + raise ValueError + if self._validate is not None: + with _quiet_callbacks(): + self._validate(value) + return value + except (Exception, SystemExit): + raise AuthError("Invalid credential payload") from None + + def read_credential(self, stream: BinaryIO) -> Credential: + payload = read_frame(stream) + if ( + set(payload) + != {"protocolVersion", "connectorSlug", "credentialType", "credential"} + or type(payload.get("protocolVersion")) is not int + or payload["protocolVersion"] != 1 + or payload["connectorSlug"] != self.connector_slug + or payload["credentialType"] != self.credential_type + ): + raise AuthError("Invalid credential envelope") + return self._validate_credential(payload["credential"]) + + def export_credential(self, stream: BinaryIO, exporter=None) -> dict[str, Any]: + try: + with _quiet_callbacks(): + credential = self._validate_credential((exporter or self._export)()) + account_id = self._account_id(credential) + if not isinstance(account_id, str) or not account_id.strip(): + raise ValueError + except (Exception, SystemExit): + raise AuthError("Local authentication is unavailable") from None + write_frame( + stream, + { + "protocolVersion": 1, + "connectorSlug": self.connector_slug, + "credentialType": self.credential_type, + "credential": credential, + }, + ) + return { + "status": "ok", + "protocolVersion": 1, + "accountId": account_id, + "credentialType": self.credential_type, + } + + def main(self, argv: Sequence[str]) -> int: + try: + if ( + len(argv) == 2 + and argv[0] == "detach" + and self._detach is not None + and len(argv[1]) == 64 + and all(c in "0123456789abcdef" for c in argv[1]) + ): + with open_pipe("rb") as stream: + credential = self.read_credential(stream) + status = "detached" + try: + with _quiet_callbacks(): + self._detach(argv[1], credential) + except SourceChanged: + status = "source_changed" + print(json.dumps({"status": status, "protocolVersion": 1})) + return 0 + if list(argv) == ["authorize"] and self._authorize is not None: + with open_pipe("wb") as stream: + metadata = self.export_credential(stream, self._authorize) + print(json.dumps(metadata, ensure_ascii=False)) + return 0 + if list(argv) == ["refresh"] and self._refresh is not None: + with open_pipe("rwb") as stream: + credential = self.read_credential(stream) + with _quiet_callbacks(): + previous_id = self._account_id(credential) + update = self._refresh(dict(credential)) + if ( + not isinstance(update, dict) + or not update.get("access_token") + or "expires_at" not in update + ): + raise AuthError( + "OAuth refresh did not return a fresh access token" + ) + refreshed = self._validate_credential({**credential, **update}) + if self._account_id(refreshed) != previous_id: + raise AuthError("OAuth account changed during refresh") + metadata = self.export_credential(stream, lambda: refreshed) + print(json.dumps(metadata, ensure_ascii=False)) + return 0 + if list(argv) == ["revoke"] and self._revoke is not None: + with open_pipe("rb") as stream: + credential = self.read_credential(stream) + with _quiet_callbacks(): + self._revoke(credential) + print(json.dumps({"status": "ok", "protocolVersion": 1})) + return 0 + if list(argv) == ["export"]: + with open_pipe("wb") as stream: + metadata = self.export_credential(stream) + print(json.dumps(metadata, ensure_ascii=False)) + return 0 + if ( + len(argv) >= 2 + and argv[0] == "run" + and argv[1] in self._allowed_commands + ): + with open_pipe("rb") as stream: + credential = self.read_credential(stream) + with native_execution_scope(): + return self._execute(credential, argv[1:]) + raise AuthError("Unsupported adapter operation") + except (Exception, SystemExit): + print(json.dumps({"status": "error", "code": "plugin_auth_adapter_failed"})) + return 1 diff --git a/plugins/lark/scripts/wegent_plugin_auth/configuration.py b/plugins/lark/scripts/wegent_plugin_auth/configuration.py new file mode 100644 index 0000000..e2e206e --- /dev/null +++ b/plugins/lark/scripts/wegent_plugin_auth/configuration.py @@ -0,0 +1,37 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Read host-validated, non-secret configuration for local auth callbacks.""" + +from __future__ import annotations + +import json +import os +import re + +from .transport import AuthError, _unique_object + + +def local_configuration() -> dict[str, str]: + """Return declared local settings; never merge them into process environment.""" + raw = os.environ.get("WEGENT_PLUGIN_AUTH_LOCAL_CONFIGURATION") + if raw is None: + return {} + try: + if len(raw.encode("utf-8")) > 16384: + raise ValueError + value = json.loads(raw, object_pairs_hook=_unique_object) + if ( + not isinstance(value, dict) + or len(value) > 16 + or any( + not re.fullmatch(r"[A-Z][A-Z0-9_]{0,63}", name) + or not isinstance(setting, str) + or not setting + or len(setting.encode("utf-8")) > 4096 + or "\x00" in setting + for name, setting in value.items() + ) + ): + raise ValueError + return value + except (ValueError, TypeError, RecursionError): + raise AuthError("Invalid local authentication configuration") from None diff --git a/plugins/lark/scripts/wegent_plugin_auth/runtime.py b/plugins/lark/scripts/wegent_plugin_auth/runtime.py new file mode 100644 index 0000000..b703176 --- /dev/null +++ b/plugins/lark/scripts/wegent_plugin_auth/runtime.py @@ -0,0 +1,184 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Delegate cloud business commands to the local native credential broker.""" + +from __future__ import annotations + +import contextlib +import json +import os +import sys +import urllib.error +import urllib.parse +import urllib.request +from contextvars import ContextVar +from pathlib import Path +from typing import Iterator, Optional, Sequence + +from .transport import AuthError + +MAX_RESPONSE_BYTES = 8 * 1024 * 1024 +_NATIVE_EXECUTION: ContextVar[bool] = ContextVar( + "wegent_native_execution", default=False +) + + +@contextlib.contextmanager +def native_execution_scope(): + token = _NATIVE_EXECUTION.set(True) + try: + yield + finally: + _NATIVE_EXECUTION.reset(token) + + +PUBLIC_ERRORS = frozenset( + { + "plugin_auth_device_not_granted", + "plugin_auth_refresh_in_progress", + "plugin_auth_reconnect_required", + "plugin_auth_account_selection_required", + "plugin_auth_backend_unavailable", + "plugin_auth_revision_conflict", + "plugin_auth_package_sync_required", + "plugin_auth_broker_busy", + "plugin_auth_invalid_command", + "plugin_auth_exchange_rejected", + } +) + + +class _NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + raise AuthError("plugin_auth_broker_unavailable") + + +def _entry_roots(entry: dict, capabilities: Path) -> Iterator[Path]: + """Use only paths recorded by the host, including its runtime copies.""" + paths = [entry.get("store_path")] + runtime_paths = entry.get("runtime") + if isinstance(runtime_paths, dict): + paths.extend(runtime_paths.get(key) for key in ("codex_link", "claude_link")) + for value in paths: + if not isinstance(value, str) or not value: + continue + path = Path(value) + path = path if path.is_absolute() else capabilities / path + yield path.resolve() + + +def _installed_id(plugin_root: Path) -> int: + home = os.environ.get("WEGENT_EXECUTOR_HOME", "") + if not home: + raise AuthError("plugin_auth_package_sync_required") + capabilities = Path(home) / "capabilities" + manifest = capabilities / "manifest.json" + if manifest.is_symlink() or manifest.stat().st_size > 8 * 1024 * 1024: + raise AuthError("plugin_auth_package_sync_required") + entries = json.loads(manifest.read_text(encoding="utf-8"))["plugins"] + root = plugin_root.resolve(strict=True) + matches = [] + for entry in entries.values(): + if entry.get("managed") is not True or entry.get("enabled") is not True: + continue + if root in _entry_roots(entry, capabilities): + matches.append(entry["installed_plugin_id"]) + if len(matches) != 1 or type(matches[0]) is not int or matches[0] <= 0: + raise AuthError("plugin_auth_package_sync_required") + return matches[0] + + +def run_account_command( + plugin_root: Path, + connector_slug: str, + arguments: Sequence[str], + *, + account_id: Optional[str] = None, +) -> str: + """Return business stdout, never credentials or native provider errors.""" + try: + url = os.environ.get("WEGENT_PLUGIN_AUTH_BROKER", "") + token = os.environ.get("WEGENT_PLUGIN_AUTH_BROKER_TOKEN", "") + parsed = urllib.parse.urlsplit(url) + if ( + parsed.scheme != "http" + or parsed.hostname != "127.0.0.1" + or not parsed.port + or parsed.username + or parsed.password + or parsed.path != "/v1/run" + or parsed.query + or parsed.fragment + or len(token) != 64 + or any(c not in "0123456789abcdef" for c in token) + ): + raise AuthError("plugin_auth_broker_unavailable") + payload = json.dumps( + { + "installed_plugin_id": _installed_id(Path(plugin_root)), + "connector_slug": connector_slug, + "account_id": account_id, + "args": list(arguments), + "working_directory": str(Path.cwd()), + } + ).encode("utf-8") + if len(payload) > 65_536: + raise AuthError("plugin_auth_invalid_command") + request = urllib.request.Request( + url, + data=payload, + headers={ + "Authorization": "Bearer " + token, + "Content-Type": "application/json", + }, + method="POST", + ) + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), _NoRedirect() + ) + try: + response = opener.open(request, timeout=200) + except urllib.error.HTTPError as error: + response = error + with response: + data = response.read(MAX_RESPONSE_BYTES + 1) + if len(data) > MAX_RESPONSE_BYTES: + raise AuthError("plugin_auth_invalid_output") + result = json.loads(data) + if not isinstance(result, dict): + raise AuthError("plugin_auth_invalid_output") + if response.status != 200: + code = result.get("error") + raise AuthError( + code if code in PUBLIC_ERRORS else "plugin_auth_execution_failed" + ) + if set(result) != {"stdout"} or not isinstance(result["stdout"], str): + raise AuthError("plugin_auth_invalid_output") + return result["stdout"] + except AuthError: + raise + except Exception: + raise AuthError("plugin_auth_broker_unavailable") from None + + +def delegate_cloud_command( + plugin_root: Path, + connector_slug: str, + arguments: Sequence[str], + *, + account_id: Optional[str] = None, +) -> Optional[int]: + """Call before local auth access. None means this is an ordinary local run.""" + if _NATIVE_EXECUTION.get(): + return None + if os.environ.get("WEGENT_PLUGIN_AUTH_MODE") != "cloud" and account_id is None: + return None + try: + sys.stdout.write( + run_account_command( + plugin_root, connector_slug, arguments, account_id=account_id + ) + ) + return 0 + except AuthError as error: + print(json.dumps({"error": str(error)})) + return 1 diff --git a/plugins/lark/scripts/wegent_plugin_auth/transport.py b/plugins/lark/scripts/wegent_plugin_auth/transport.py new file mode 100644 index 0000000..6008070 --- /dev/null +++ b/plugins/lark/scripts/wegent_plugin_auth/transport.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Bounded frames over a host-owned descriptor; never a model-visible channel.""" + +from __future__ import annotations + +import json +import os +import socket +import stat +import struct +import sys +from typing import Any, BinaryIO + +MAX_FRAME_BYTES = 65536 + + +class AuthError(RuntimeError): + """A failure whose message contains no credential material.""" + + +def open_pipe(mode: str) -> BinaryIO: + try: + if mode not in {"rb", "wb", "rwb"}: + raise ValueError + if "WEGENT_PLUGIN_AUTH_PORT" in os.environ: + return _open_socket(mode) + fd = int(os.environ["WEGENT_PLUGIN_AUTH_FD"]) + if fd < 3: + raise ValueError + info = os.fstat(fd) + if not (stat.S_ISFIFO(info.st_mode) or stat.S_ISSOCK(info.st_mode)): + raise ValueError + if mode == "rwb" and not stat.S_ISSOCK(info.st_mode): + raise ValueError + os.set_inheritable(fd, False) + return os.fdopen(fd, "r+b" if mode == "rwb" else mode) + except (KeyError, ValueError, OSError): + raise AuthError("A dedicated broker pipe is required") from None + + +def _open_socket(mode: str) -> BinaryIO: + """Cross-platform native transport; stdin carries a one-use capability only.""" + if "WEGENT_PLUGIN_AUTH_FD" in os.environ: + raise AuthError("Ambiguous broker transport") + port = int(os.environ.pop("WEGENT_PLUGIN_AUTH_PORT")) + if not 1 <= port <= 65535: + raise AuthError("Invalid broker transport") + nonce = _read_exact(sys.stdin.buffer, 32) + with socket.create_connection(("127.0.0.1", port), timeout=5) as connection: + connection.set_inheritable(False) + connection.sendall(nonce) + return connection.makefile(mode) + + +def _read_exact(stream: BinaryIO, length: int) -> bytes: + result = bytearray() + while len(result) < length: + chunk = stream.read(length - len(result)) + if not chunk: + raise AuthError("Incomplete credential frame") + result.extend(chunk) + return bytes(result) + + +def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError + result[key] = value + return result + + +def read_frame(stream: BinaryIO) -> dict[str, Any]: + size = struct.unpack("!I", _read_exact(stream, 4))[0] + if not 1 <= size <= MAX_FRAME_BYTES: + raise AuthError("Invalid credential frame size") + raw = _read_exact(stream, size) + try: + payload = json.loads(raw.decode("utf-8"), object_pairs_hook=_unique_object) + if not isinstance(payload, dict): + raise ValueError + # Reject NaN/Infinity, including nested values accepted by json.loads. + json.dumps(payload, allow_nan=False) + return payload + except (ValueError, TypeError, RecursionError): + raise AuthError("Invalid credential frame") from None + + +def write_frame(stream: BinaryIO, payload: dict[str, Any]) -> None: + try: + raw = json.dumps( + payload, ensure_ascii=False, separators=(",", ":"), allow_nan=False + ).encode("utf-8") + except (ValueError, TypeError, RecursionError): + raise AuthError("Invalid credential frame") from None + if not 1 <= len(raw) <= MAX_FRAME_BYTES: + raise AuthError("Invalid credential frame size") + data = struct.pack("!I", len(raw)) + raw + offset = 0 + while offset < len(data): + count = stream.write(data[offset:]) + if count is None or count <= 0: + raise AuthError("Incomplete credential frame") + offset += count + stream.flush() diff --git a/plugins/lark/scripts/wegent_plugin_auth/vendor.json b/plugins/lark/scripts/wegent_plugin_auth/vendor.json new file mode 100644 index 0000000..5e66d36 --- /dev/null +++ b/plugins/lark/scripts/wegent_plugin_auth/vendor.json @@ -0,0 +1,13 @@ +{ + "sdk": "wegent-plugin-auth-python", + "version": "0.7.1", + "protocolVersion": 1, + "files": { + "LICENSE": "e7e430d6f6693d9e1fe09961d873d7c8f6938bfd57746d60d18b895c62b15760", + "__init__.py": "b6ce9a286c0a06ecfe0652d5045e488eba98bcc2aad41f5ebd50e4b3aa28c98c", + "adapter.py": "c51549ca0e1503f6d714a52bdf6ddc265e4067aa8d4470e5c6c9077d10f5d8e6", + "configuration.py": "0bb293d2c82db21c5eb19a88cea884fa758700c4350e93baa238b87c5d5e08ae", + "runtime.py": "3fbe06a3334acf36fe9687cc9dfbdc802d804982b51ddf064880ec68e3adc84d", + "transport.py": "664e4a848c9fea879f729a967191c37d7ab02a0180c0f02bced4cd62c4199c34" + } +} diff --git a/plugins/lark/skills/lark-approval/SKILL.md b/plugins/lark/skills/lark-approval/SKILL.md index 9447c1a..bc60e84 100644 --- a/plugins/lark/skills/lark-approval/SKILL.md +++ b/plugins/lark/skills/lark-approval/SKILL.md @@ -3,11 +3,11 @@ name: lark-approval description: "飞书审批:查询和处理审批待办/已办/实例,搜索可发起审批定义、查看定义详情并发起原生审批实例。当用户要处理审批任务、查看审批实例、搜索或发起审批时使用。审批待办不是飞书任务;非审批类待办走 lark-task。不负责创建审批定义;三方审批定义不走原生提单。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 **CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** diff --git a/plugins/lark/skills/lark-attendance/SKILL.md b/plugins/lark/skills/lark-attendance/SKILL.md index ea7db32..9950c74 100644 --- a/plugins/lark/skills/lark-attendance/SKILL.md +++ b/plugins/lark/skills/lark-attendance/SKILL.md @@ -3,11 +3,11 @@ name: lark-attendance description: "飞书考勤打卡:查询自己的考勤打卡记录" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # attendance (v1) diff --git a/plugins/lark/skills/lark-base/SKILL.md b/plugins/lark/skills/lark-base/SKILL.md index d8d7ea1..0bfbb20 100644 --- a/plugins/lark/skills/lark-base/SKILL.md +++ b/plugins/lark/skills/lark-base/SKILL.md @@ -3,11 +3,11 @@ name: lark-base description: "飞书多维表格(Base)操作:建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、workflow、角色权限;遇到 Base/多维表格/bitable 或 /base/ 链接时使用。文件导入转 lark-drive,认证/授权转 lark-shared。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # base diff --git a/plugins/lark/skills/lark-calendar/SKILL.md b/plugins/lark/skills/lark-calendar/SKILL.md index cd15fa2..7e5e48f 100644 --- a/plugins/lark/skills/lark-calendar/SKILL.md +++ b/plugins/lark/skills/lark-calendar/SKILL.md @@ -3,11 +3,11 @@ name: lark-calendar description: "飞书日历:管理日历日程和会议室。查看/搜索日程、创建/更新日程、管理参会人、查询忙闲和推荐时段、预定会议室。当用户需要查看日程安排、创建/修改会议、查询/预定会议室时使用。不负责:查询过去的视频会议记录(走 lark-vc)、待办任务(走 lark-task)。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # calendar (v4) diff --git a/plugins/lark/skills/lark-contact/SKILL.md b/plugins/lark/skills/lark-contact/SKILL.md index 31c161d..e9247a2 100644 --- a/plugins/lark/skills/lark-contact/SKILL.md +++ b/plugins/lark/skills/lark-contact/SKILL.md @@ -3,11 +3,11 @@ name: lark-contact description: "飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 open_id 反查姓名 / 部门 / 邮箱 / 联系方式 / 个人状态 / 签名。当用户提到某人姓名要下一步发消息 / 排日程,或拿到 open_id 想查具体信息时使用。不负责部门树遍历、按部门列员工、组织架构图,这类需求走原生 OpenAPI。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 ## 选哪个命令 diff --git a/plugins/lark/skills/lark-doc/SKILL.md b/plugins/lark/skills/lark-doc/SKILL.md index 439fcf6..af8c460 100644 --- a/plugins/lark/skills/lark-doc/SKILL.md +++ b/plugins/lark/skills/lark-doc/SKILL.md @@ -3,11 +3,11 @@ name: lark-doc description: "飞书云文档(Docx / Wiki 文档):读取和编辑飞书文档内容。当用户给出文档 URL 或 token,或需要查看、创建、编辑文档、插入或下载文档图片附件时使用。文档中嵌入的电子表格、多维表格、画板,先用本 skill 提取 token 再切到对应 skill。当用户给出 doubao.com 的 /docx/ 或 /wiki/ URL/token 时,也应直接使用本 skill;路由依据是 URL 路径模式和 token,而不是域名。不负责文档评论管理,也不负责表格或 Base 的数据操作。当用户明确要操作飞书思维笔记时,也使用本 skill。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # docs diff --git a/plugins/lark/skills/lark-drive/SKILL.md b/plugins/lark/skills/lark-drive/SKILL.md index 8ee6239..a7d66bc 100644 --- a/plugins/lark/skills/lark-drive/SKILL.md +++ b/plugins/lark/skills/lark-drive/SKILL.md @@ -3,11 +3,11 @@ name: lark-drive description: "飞书云空间(云盘/云存储):管理 Drive 文件和文件夹,包含上传/下载、创建文件夹、复制/移动/删除、查看元数据、评论/权限/订阅、标题、版本和本地文件导入。用户需要整理云盘目录、处理云空间资源 URL/token、判断链接类型/真实 token/标题,或导入 Word/Markdown/Excel/CSV/PPTX/.base 为 docx/sheet/bitable/slides 时使用;doubao.com 云空间 URL/token 也按资源路径和 token 路由,不回退 WebFetch。不负责:文档内容编辑(走 lark-doc)、表格/Base 表内数据操作(走 lark-sheets/lark-base)、知识空间节点/成员管理(走 lark-wiki)、原生 Markdown 文件读写/patch/diff(走 lark-markdown)。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # drive (v1) diff --git a/plugins/lark/skills/lark-event/SKILL.md b/plugins/lark/skills/lark-event/SKILL.md index 49c3f10..39bed6f 100644 --- a/plugins/lark/skills/lark-event/SKILL.md +++ b/plugins/lark/skills/lark-event/SKILL.md @@ -3,11 +3,11 @@ name: lark-event description: "飞书/Lark 实时事件监听/订阅/消费:通过 `lark-cli event consume EventKey` 以 NDJSON 流式输出事件(覆盖 IM 消息/表情/群聊变更、任务更新、视频会议开始/入会/结束、妙记生成、画板更新等)。适用于飞书机器人、实时消息处理、长时订阅者、流式 webhook/推送处理。支持 `--max-events` / `--timeout` 有界运行,以及 stderr ready-marker 约定——面向以子进程方式运行的 AI agent。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # Lark Events diff --git a/plugins/lark/skills/lark-im/SKILL.md b/plugins/lark/skills/lark-im/SKILL.md index 1361282..9ee106a 100644 --- a/plugins/lark/skills/lark-im/SKILL.md +++ b/plugins/lark/skills/lark-im/SKILL.md @@ -3,11 +3,11 @@ name: lark-im description: "飞书即时通讯:收发消息和管理群聊。发送和回复消息、搜索聊天记录、管理群聊成员、上传下载图片和文件(支持大文件分片下载)、管理表情回复、发送应用内/短信/电话加急、发送和处理交互卡片(Interactive Card)、监听卡片按钮回调(card.action.trigger)。当用户需要发消息、查看或搜索聊天记录、下载聊天中的文件、查看群成员、搜索群、创建群聊或话题群、管理标记数据、管理 Feed 置顶(添加/移除/查询置顶会话)、管理标签数据、处理卡片回调时使用。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # im (v1) diff --git a/plugins/lark/skills/lark-mail/SKILL.md b/plugins/lark/skills/lark-mail/SKILL.md index 678f32a..f031e71 100644 --- a/plugins/lark/skills/lark-mail/SKILL.md +++ b/plugins/lark/skills/lark-mail/SKILL.md @@ -3,11 +3,11 @@ name: lark-mail description: "飞书邮箱:当用户提到起草邮件、写邮件、草稿、发送/回复/转发邮件、查阅邮件、看邮件、搜索邮件、邮件文件夹、邮件标签、邮件联系人、监听新邮件、邮件收信规则等时使用;仅用于邮件相关意图。不用于文档/表格/日历/授权配置/纯通讯录查询/IM 聊天任务。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # mail (v1) diff --git a/plugins/lark/skills/lark-markdown/SKILL.md b/plugins/lark/skills/lark-markdown/SKILL.md index a1cb39a..1c5f0ba 100644 --- a/plugins/lark/skills/lark-markdown/SKILL.md +++ b/plugins/lark/skills/lark-markdown/SKILL.md @@ -3,11 +3,11 @@ name: lark-markdown description: "飞书 Markdown:查看、创建、上传、编辑和比较 Markdown 文件。当用户需要创建或编辑 Markdown 文件、读取、修改、局部 patch 或比较差异时使用。不负责将 Markdown 导入为飞书在线文档,也不负责文件搜索、权限、评论、移动、删除等云空间管理操作。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # markdown (v1) diff --git a/plugins/lark/skills/lark-minutes/SKILL.md b/plugins/lark/skills/lark-minutes/SKILL.md index fd0e5e8..cfaf7ba 100644 --- a/plugins/lark/skills/lark-minutes/SKILL.md +++ b/plugins/lark/skills/lark-minutes/SKILL.md @@ -3,11 +3,11 @@ name: lark-minutes description: "飞书妙记:搜索妙记、查看妙记基础信息、下载/上传音视频、读取或编辑妙记的产物内容、改标题、替换说话人/关键词。当给出minute_token、本地音视频文件,要查/改/转妙记产物时使用;本地音视频转纪要/逐字稿优先走本 skill,不要用 ffmpeg/whisper 本地转写。不负责:获取会议关联妙记,或仅按自然语言标题定位纪要" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # minutes (v1) diff --git a/plugins/lark/skills/lark-note/SKILL.md b/plugins/lark/skills/lark-note/SKILL.md index 9cf5739..dc7c9d2 100644 --- a/plugins/lark/skills/lark-note/SKILL.md +++ b/plugins/lark/skills/lark-note/SKILL.md @@ -3,11 +3,11 @@ name: lark-note description: "飞书会议纪要(Note)直查:已知 note_id 时查询纪要详情、展示类型、关联文档 token,并读取 unified 原始逐字记录。当用户已持有 note_id,或从文档显式 vc-node-id 获得 note_id 时使用。不负责会议/日程/妙记定位、文档标题搜索或 Docx 正文读取。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # note (v1) diff --git a/plugins/lark/skills/lark-okr/SKILL.md b/plugins/lark/skills/lark-okr/SKILL.md index c6c3fe9..f304fd5 100644 --- a/plugins/lark/skills/lark-okr/SKILL.md +++ b/plugins/lark/skills/lark-okr/SKILL.md @@ -3,11 +3,11 @@ name: lark-okr description: "飞书 OKR:管理目标与关键结果。查看和编辑 OKR 周期、目标、关键结果、对齐关系、量化指标和进展记录。当用户需要查看或创建 OKR、管理目标和关键结果、查看对齐关系时使用。不负责:待办任务管理(lark-task)、日程/会议安排(lark-calendar)、绩效评估" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # okr (v2) diff --git a/plugins/lark/skills/lark-openapi-explorer/SKILL.md b/plugins/lark/skills/lark-openapi-explorer/SKILL.md index 97e929e..608c084 100644 --- a/plugins/lark/skills/lark-openapi-explorer/SKILL.md +++ b/plugins/lark/skills/lark-openapi-explorer/SKILL.md @@ -3,11 +3,11 @@ name: lark-openapi-explorer description: "飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未经 CLI 封装的原生 OpenAPI 接口。当用户的需求无法被现有 lark-* skill 或 lark-cli 已注册命令满足,需要查找并调用原生飞书 OpenAPI 时使用。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # OpenAPI Explorer diff --git a/plugins/lark/skills/lark-shared/SKILL.md b/plugins/lark/skills/lark-shared/SKILL.md index 009664a..545a623 100644 --- a/plugins/lark/skills/lark-shared/SKILL.md +++ b/plugins/lark/skills/lark-shared/SKILL.md @@ -3,11 +3,11 @@ name: lark-shared description: "用于 lark-cli 的配置与授权任务:auth login/status/logout、用户与机器人身份、业务域权限(--domain,含 all/docs/drive)、缺失 scope、撤销授权,或处理 _notice JSON。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # lark-cli 共享规则 @@ -22,44 +22,17 @@ description: "用于 lark-cli 的配置与授权任务:auth login/status/logou - 路径和含空格参数必须保持为独立参数并完整引用,不得拼接成一段命令字符串后执行。 - 参考文档中的 `python3` 在 macOS/Linux 原样使用;Windows PowerShell 必须替换为 `py -3`,或使用 workspace dependency loader 返回的绝对 `python.exe`。 -## 配置初始化 +## 配置与认证 -首次使用需运行 `lark-cli config init` 完成应用配置。 +在 Wegent 原连接入口完成应用配置和用户浏览器 OAuth。浏览器链接由本机原生脚本打开,认证完成后自动交接;不要从模型进程读取、显示或上传密钥与 Token。 -当你帮用户初始化配置时,使用background方式使用下面的命令发起配置应用流程,启动后读取输出,从中提取授权链接并发给用户。 - -**URL 转发规则**:当命令输出 `verification_url`、`verification_uri_complete`、`console_url` 等 URL 字段时:**必须生成二维码**:你必须调用 `lark-cli auth qrcode` 将 URL 转为二维码并展示给用户,这是必须步骤,不要跳过。优先生成 PNG 二维码(--output);仅当用户明确要求时才使用 ASCII(--ascii)。**URL 输出规则**:将 URL 视为不可修改的 opaque string,不要做任何修改(包括 URL 编码/解码、添加空格或标点、重新拼接 query),二维码和链接请一起展示给用户。 - -```bash -# 发起配置(该命令会阻塞直到用户打开链接并完成操作或过期) -lark-cli config init --new -``` - -## 认证 - -### 认证任务速查 - -认证、scope、业务域、登录态、退出登录态、撤销授权问题都走本技能。 - -| 用户意图 | 首选命令 / 回答 | -|---|---| -| 获取全部权限 | `lark-cli auth login --domain all --no-wait --json` | -| 按业务域授权 | `lark-cli auth login --domain docs --domain drive --no-wait --json`;`--domain` 可重复,也可用逗号分隔 | -| 指定单个 scope 授权 | `lark-cli auth login --scope "" --no-wait --json` | -| 检查当前登录态、是谁登录、token 是否有效 | `lark-cli auth status --json --verify`;回答时引用 `identity`、`verified`、`identities.user.status`、`identities.user.userName`、`identities.user.openId`(用户 open id)、`identities.user.tokenStatus`、`identities.user.scope` | -| 快速查看当前身份状态 | `lark-cli whoami`;实际生效的那一个身份 | -| 退出当前机器的用户登录态 | `lark-cli auth logout --json`;`loggedOut:true` 表示注销成功 | -| bot 缺少权限 | 不要执行 `auth login`;引导用户在开发者后台开通 bot scope,优先复用错误里的 `console_url` | -| 取消用户对应用的全部服务端授权 | `auth logout` 只清本机登录态;服务端授权需用户在飞书授权管理页取消 | -| 只取消一个 scope | CLI 不支持单独撤销一个已授予 scope;可重新走最小 scope 授权,或让用户在授权管理页处理 | - -机器读取 JSON 时需要关闭 `_notice` 干扰。插件包装器已经在所有平台设置 -`LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1` 和 `LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1`, -直接执行逻辑命令即可: - -```bash -lark-cli auth status --json --verify -``` +- `auth status` 在托管模式下返回 `status` 和 `accountId`,不沿用原 CLI 的 `identities` JSON 结构。 +- 用户身份为 `lark` OAuth 连接,应用身份为 `lark-app` 密码类连接。使用 `--as bot` 时必须获得该应用连接的云端授权,不能借用户 OAuth 提权。 +- 本机托管状态下执行 `auth login --scope ` 或 `auth login --domain ` 只记录非敏感的授权范围,并返回需要重新连接。随后在原连接入口重新连接,完成指定范围的浏览器授权。云端不执行此操作,需回到源设备处理。 +- 应用 scope 在开发者后台开通;不要为 bot 执行用户 OAuth。 +- 用户退出在原连接入口执行,由宿主断开云端,再清理本机用户 Token。撤销由宿主私有回调处理,不使用裸 CLI 的 logout。 +- `--profile`、`--workspace`、认证/代理覆盖、调试插件等不允许进入托管业务执行;本次同步原默认配置所选的应用与用户,切换账号需在本机完成并重新连接。 +- 原 CLI 的写操作确认规则保持有效。不得自动追加 `--yes`;只有用户明确授权相应写操作时,按 CLI 的要求传入确认。 ### 身份类型 @@ -102,45 +75,9 @@ lark-cli auth login --scope "" # 按具体 scope 授权(推 **规则**:auth login 必须指定范围(`--domain` 或 `--scope`)。多次 login 的 scope 会累积(增量授权)。 -#### Agent 代理发起认证(推荐) - -当你作为 AI agent 需要帮用户完成认证时,优先使用 split-flow,避免在同一轮对话中阻塞等待用户授权: - -```bash -# 发起授权(立即返回 device_code 和 verification_url) -lark-cli auth login --scope "calendar:calendar:readonly" --no-wait --json -``` - -拿到 `verification_url` 后,将它原样作为本轮最终消息发给用户,并结束本轮/交还控制权。不要在同一轮中展示 URL 后立刻执行 `--device-code` 阻塞轮询;在不透传中间输出的 agent harness 里,这会导致用户永远看不到 URL。 - -用户回复已完成授权后,再在后续步骤执行: - -```bash -lark-cli auth login --device-code -``` - -**Split-Flow 完整步骤**: - -**第一步:发起授权(当前轮)** - -1. 执行 `lark-cli auth login --scope "xxx" --no-wait --json`(必须加 `--no-wait --json`) -2. 从 JSON 输出中提取 `verification_url` 和 `device_code` -3. 生成二维码:`lark-cli auth qrcode --output "xxx"` -4. 将 URL 和二维码展示给用户(先 URL,后二维码) -5. **结束本轮对话前,必须明确告知用户**:"请完成授权后,回来告诉我已授权完成,我会帮你完成后续步骤" - -**第二步:完成授权(后续轮)** - -1. 等待用户回复"已完成授权" -2. **由你(AI agent)亲自执行**:`lark-cli auth login --device-code ` -3. 此命令会轮询授权状态并完成登录 -4. 如果返回授权成功,流程结束 - -**关键规则**: +#### 托管状态下增量授权 -- **你必须亲自执行 `--device-code` 命令**,不要指示用户自行执行 -- **不要在同一轮中展示 URL 后立刻执行 `--device-code`**,这会导致用户看不到 URL -- **禁止缓存 `verification_url` 或 `device_code`**:每次需要授权时,必须重新执行 `lark-cli auth login --no-wait --json` 生成新的链接。不要将授权链接和 device code 存入上下文供后续复用 +本机按上面的 `auth login --scope/--domain` 记录范围后,通过 Wegent 原连接入口重新连接。云端只报告缺失 scope 与开发者后台链接,不发起第二份授权、不请求 device code 或 Token。 ## 更新检查 diff --git a/plugins/lark/skills/lark-sheets/SKILL.md b/plugins/lark/skills/lark-sheets/SKILL.md index 2f4859b..2ce8c64 100644 --- a/plugins/lark/skills/lark-sheets/SKILL.md +++ b/plugins/lark/skills/lark-sheets/SKILL.md @@ -3,11 +3,11 @@ name: lark-sheets description: "飞书电子表格:创建和操作电子表格。支持创建表格、管理工作表与行列结构(增删/合并/调整尺寸/隐藏/冻结)、读写单元格(值/公式/样式/批注/单元格图片)、查找替换、多操作原子批量更新,以及图表、透视表、条件格式、筛选器、迷你图、浮动图片等对象的创建与维护。当用户需要创建电子表格、管理工作表、批量读写或编辑数据、统计汇总与可视化、表格美化、公式计算(含 Excel 公式迁移)、金融/财务建模(DCF、三张表、预算、Sensitivity 等)等任务时使用。若用户是想按名称或关键词搜索云空间(云盘/云存储)里的表格文件,请改用 lark-drive 的 drive +search 先定位资源。当用户给出 doubao.com 的 /sheets/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # sheets diff --git a/plugins/lark/skills/lark-skill-maker/SKILL.md b/plugins/lark/skills/lark-skill-maker/SKILL.md index 52971c7..84837ec 100644 --- a/plugins/lark/skills/lark-skill-maker/SKILL.md +++ b/plugins/lark/skills/lark-skill-maker/SKILL.md @@ -3,11 +3,11 @@ name: lark-skill-maker description: "创建 lark-cli 的自定义 Skill。当用户需要把飞书 API 操作封装成可复用的 Skill(包装原子 API 或编排多步流程)时使用。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # Skill Maker diff --git a/plugins/lark/skills/lark-slides/SKILL.md b/plugins/lark/skills/lark-slides/SKILL.md index 571d7fd..68dd06c 100644 --- a/plugins/lark/skills/lark-slides/SKILL.md +++ b/plugins/lark/skills/lark-slides/SKILL.md @@ -3,11 +3,11 @@ name: lark-slides description: "飞书幻灯片:创建和编辑幻灯片。创建演示文稿、读取幻灯片内容、管理幻灯片页面(创建、删除、读取、局部替换)。当用户需要创建或编辑幻灯片、读取或修改单个页面时使用。当用户给出 doubao.com 的 /slides/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。不负责:云文档内容编辑(走 lark-doc)、云文档里的独立画板对象(走 lark-whiteboard,注意 slide 内嵌的流程图/架构图仍属本 skill)、上传或下载普通文件(走 lark-drive)。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # slides (v1) diff --git a/plugins/lark/skills/lark-task/SKILL.md b/plugins/lark/skills/lark-task/SKILL.md index 01adb52..c32083a 100644 --- a/plugins/lark/skills/lark-task/SKILL.md +++ b/plugins/lark/skills/lark-task/SKILL.md @@ -3,11 +3,11 @@ name: lark-task description: "飞书任务:管理任务、清单和任务智能体。创建待办任务、查看和更新任务状态、拆分子任务、组织任务清单、分配协作成员、上传任务附件、注册或注销任务智能体、更新任务智能体的主页数据、写入智能体任务记录。当用户需要创建待办事项、查看任务列表、跟踪任务进度、管理项目清单或给他人分配任务、为任务上传附件文件、注册注销任务智能体、更新智能体主页数据、写入任务记录时使用。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # task (v2) diff --git a/plugins/lark/skills/lark-vc-agent/SKILL.md b/plugins/lark/skills/lark-vc-agent/SKILL.md index 9de95c2..149de94 100644 --- a/plugins/lark/skills/lark-vc-agent/SKILL.md +++ b/plugins/lark/skills/lark-vc-agent/SKILL.md @@ -3,11 +3,11 @@ name: lark-vc-agent description: "飞书视频会议会中能力:用于让应用机器人真实加入或离开正在进行的会议,并读取当前身份可见的会中事件、发送会中文本消息或会中表情。适用于用户询问正在开的会议发生了什么、谁在发言、是否共享内容,或需要发现当前可读的进行中会议 ID。不负责已结束会议搜索、参会人快照、纪要、逐字稿或录制查询,这些使用 lark-vc 技能。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # vc-agent (v1) diff --git a/plugins/lark/skills/lark-vc/SKILL.md b/plugins/lark/skills/lark-vc/SKILL.md index 9aba7bb..831d826 100644 --- a/plugins/lark/skills/lark-vc/SKILL.md +++ b/plugins/lark/skills/lark-vc/SKILL.md @@ -3,11 +3,11 @@ name: lark-vc description: "飞书视频会议:搜索历史会议记录、查询会议纪要(总结/待办/章节/逐字稿)、查询参会人快照。当用户查询已结束的会议、获取会议产物(纪要/妙记)、查看参会人时使用;查询未来日程走 lark-calendar。不负责:Agent 真实入会/离会、会中实时事件(走 lark-vc-agent)。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # vc (v1) diff --git a/plugins/lark/skills/lark-whiteboard/SKILL.md b/plugins/lark/skills/lark-whiteboard/SKILL.md index 7457259..27d1778 100644 --- a/plugins/lark/skills/lark-whiteboard/SKILL.md +++ b/plugins/lark/skills/lark-whiteboard/SKILL.md @@ -5,11 +5,11 @@ description: > 当用户需要查看画板内容、导出画板图片、编辑画板时使用此 skill。不负责:飞书云文档内容编辑(lark-doc)、文档内嵌电子表格/Base(lark-sheets / lark-base)。 --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 > [!IMPORTANT] > - 运行 `lark-cli --version`,确认可用,无需询问用户。 diff --git a/plugins/lark/skills/lark-wiki/SKILL.md b/plugins/lark/skills/lark-wiki/SKILL.md index f7771b6..16cc5ed 100644 --- a/plugins/lark/skills/lark-wiki/SKILL.md +++ b/plugins/lark/skills/lark-wiki/SKILL.md @@ -3,11 +3,11 @@ name: lark-wiki description: "飞书知识库:管理知识空间、空间成员和文档节点。创建和查询知识空间、查看和管理空间成员、管理节点层级结构、在知识库中组织文档和快捷方式。当用户需要在知识库中查找或创建文档、浏览知识空间结构、查看或管理空间成员、移动或复制节点时使用。当用户给出 doubao.com 的 /wiki/ URL/token 时,也应直接使用本 skill,不要因为域名不是飞书而回退到 WebFetch;路由依据是 URL 路径模式和 token,而不是域名。不负责:上传文件到知识库节点下(走 lark-drive)、编辑文档/表格/Base 内容(走 lark-doc / lark-sheets / lark-base)。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # wiki (v2) diff --git a/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md b/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md index e68bf26..5573318 100644 --- a/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md +++ b/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md @@ -3,11 +3,11 @@ name: lark-workflow-meeting-summary description: "会议纪要整理工作流:汇总指定时间范围内的会议纪要并生成结构化报告。当用户需要整理会议纪要、生成会议周报、回顾一段时间内的会议内容时使用。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # 会议纪要汇总工作流 diff --git a/plugins/lark/skills/lark-workflow-standup-report/SKILL.md b/plugins/lark/skills/lark-workflow-standup-report/SKILL.md index d15ff6a..8b32c2a 100644 --- a/plugins/lark/skills/lark-workflow-standup-report/SKILL.md +++ b/plugins/lark/skills/lark-workflow-standup-report/SKILL.md @@ -3,11 +3,11 @@ name: lark-workflow-standup-report description: "日程待办摘要:编排 calendar +agenda 和 task +get-my-tasks,生成指定日期的日程与未完成任务摘要。适用于了解今天/明天/本周的安排。" --- -## Wegent 本地运行 +## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 配置和用户 OAuth 均由官方 CLI 在本机管理。不要要求用户在对话中粘贴 App Secret 或 Access Token,也不要读取、记录或上传 `~/.lark-cli` 或系统钥匙串中的认证信息。配置、扫码、企业审批或增量授权需要用户操作时,展示 CLI 原样输出的 URL/二维码并暂停等待。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # 日程待办摘要工作流 From fe43a1fdcaa8784dd56183a1eb462c34c2915fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=BF=8A=E9=BE=99?= Date: Wed, 9 Sep 2026 18:18:41 +0800 Subject: [PATCH 2/4] fix(lark): make native package checks portable on Windows --- plugins/lark/.wework-build/lark-auth/build.py | 5 +++-- plugins/lark/scripts/tests/test_runtime.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/lark/.wework-build/lark-auth/build.py b/plugins/lark/.wework-build/lark-auth/build.py index 8d00292..313de76 100644 --- a/plugins/lark/.wework-build/lark-auth/build.py +++ b/plugins/lark/.wework-build/lark-auth/build.py @@ -42,7 +42,7 @@ def prepare(directory, archive): # Intercept every upstream keychain entry point in managed business mode, # including direct UAT helpers that bypass Factory.WithKeychain. path = root / "internal/keychain/keychain.go" - value = path.read_text() + value = path.read_text(encoding="utf-8") changes = { "func Get(service, account string) (string, error) {": "func Get(service, account string) (string, error) {\n if WegentAccess != nil { return WegentAccess.Get(service, account) }", "func Set(service, account, data string) error {": "func Set(service, account, data string) error {\n if WegentAccess != nil { return WegentAccess.Set(service, account, data) }", @@ -54,7 +54,8 @@ def prepare(directory, archive): value = value.replace(old, new) path.write_text( value - + "\n// WegentAccess is set only in the native managed process.\nvar WegentAccess KeychainAccess\n" + + "\n// WegentAccess is set only in the native managed process.\nvar WegentAccess KeychainAccess\n", + encoding="utf-8", ) return root diff --git a/plugins/lark/scripts/tests/test_runtime.py b/plugins/lark/scripts/tests/test_runtime.py index a09432e..07a7051 100644 --- a/plugins/lark/scripts/tests/test_runtime.py +++ b/plugins/lark/scripts/tests/test_runtime.py @@ -170,7 +170,7 @@ def test_source_lock_blocks_another_real_process(self): stdout=subprocess.PIPE, ) try: - self.assertEqual(child.stdout.readline(), b"locked\n") + self.assertEqual(child.stdout.readline().rstrip(b"\r\n"), b"locked") probe = subprocess.Popen( [ sys.executable, @@ -184,7 +184,7 @@ def test_source_lock_blocks_another_real_process(self): self.assertIsNone(probe.poll()) child.communicate(timeout=5) out, _ = probe.communicate(timeout=5) - self.assertEqual(out, b"entered\n") + self.assertEqual(out.rstrip(b"\r\n"), b"entered") finally: if child.poll() is None: child.kill() From 6b42a4d7032c5008f94041f8fd8c14852fc787df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=BF=8A=E9=BE=99?= Date: Wed, 9 Sep 2026 18:29:19 +0800 Subject: [PATCH 3/4] fix(lark): preserve local event daemon execution boundaries --- plugins/lark/.wework-build/lark-auth/build.py | 15 ++++++++++++++- .../.wework-build/lark-auth/overlay/business.go | 3 +++ plugins/lark/README.md | 2 ++ plugins/lark/scripts/lark_cli.py | 6 ++++++ plugins/lark/scripts/tests/test_runtime.py | 7 ++++++- plugins/lark/skills/lark-event/SKILL.md | 2 ++ 6 files changed, 33 insertions(+), 2 deletions(-) diff --git a/plugins/lark/.wework-build/lark-auth/build.py b/plugins/lark/.wework-build/lark-auth/build.py index 313de76..a1215d1 100644 --- a/plugins/lark/.wework-build/lark-auth/build.py +++ b/plugins/lark/.wework-build/lark-auth/build.py @@ -57,6 +57,19 @@ def prepare(directory, archive): + "\n// WegentAccess is set only in the native managed process.\nvar WegentAccess KeychainAccess\n", encoding="utf-8", ) + # The bundled executable has a private dispatcher before the upstream CLI. + # Preserve the local event daemon's self-spawn entry point. + startup = root / "internal/event/consume/startup.go" + value = startup.read_text(encoding="utf-8") + old = "cmd := exec.Command(exe, args...)" + if value.count(old) != 1: + raise ValueError("Upstream event daemon boundary changed") + startup.write_text( + value.replace( + old, 'cmd := exec.Command(exe, append([]string{"local"}, args...)...)' + ), + encoding="utf-8", + ) return root @@ -80,7 +93,7 @@ def build(root, output, target): check=True, ) content = raw.read_bytes() - output.write_bytes(lzma.compress(content, preset=9)) + output.write_bytes(lzma.compress(content, preset=6)) metadata = { "nativeProtocolVersion": 1, "target": target, diff --git a/plugins/lark/.wework-build/lark-auth/overlay/business.go b/plugins/lark/.wework-build/lark-auth/overlay/business.go index 79d8efa..ce564b3 100644 --- a/plugins/lark/.wework-build/lark-auth/overlay/business.go +++ b/plugins/lark/.wework-build/lark-auth/overlay/business.go @@ -25,6 +25,9 @@ func allowed(args []string) bool { if len(args) == 0 || !products[args[0]] { return false } + if args[0] == "event" && (len(args) < 2 || (args[1] != "list" && args[1] != "schema")) { + return false + } for _, arg := range args { name := strings.SplitN(arg, "=", 2)[0] switch name { diff --git a/plugins/lark/README.md b/plugins/lark/README.md index f5af9c6..1332708 100644 --- a/plugins/lark/README.md +++ b/plugins/lark/README.md @@ -11,6 +11,8 @@ 始终通过 `scripts/run-lark-cli.sh` 或 Windows 的 `scripts/run-lark-cli.ps1` 调用业务命令。托管模式禁止 CLI 自行登录、修改配置或切换 profile。需要新增 scope 时,在来源设备通过 wrapper 发起 `auth login --scope ...` 或 `--domain ...`,再在 Wegent 原生连接界面重新连接。云端遇到权限不足时返回来源设备处理。写操作保留原 CLI 的确认规则。 +事件总线订阅依赖本地常驻进程及应用密钥,应在来源设备使用 `--as bot` 运行。托管调用仅开放 `event list` 和 `event schema`;不在云端启动后台事件进程。托管请求正文请使用参数或 `@文件`,不通过 stdin 传入。 + ## 认证边界 用户刷新令牌与应用密钥仅进入私有适配器,不通过命令行、环境变量或业务输出传递。托管业务命令使用内存凭据提供器,禁用本地 keychain、用户插件与配置覆盖;携带凭据的 HTTP 请求只允许对应品牌的官方 HTTPS API。 diff --git a/plugins/lark/scripts/lark_cli.py b/plugins/lark/scripts/lark_cli.py index bed7c8a..e8534be 100644 --- a/plugins/lark/scripts/lark_cli.py +++ b/plugins/lark/scripts/lark_cli.py @@ -54,6 +54,12 @@ def identity(arguments): def business_args(arguments): + if ( + arguments + and arguments[0] == "event" + and (len(arguments) < 2 or arguments[1] not in ("list", "schema")) + ): + raise AuthError("plugin_auth_local_event_required") if arguments == ["--ready"] or arguments[:2] == ["auth", "status"]: return ["account-status"] if not arguments or arguments[0] in { diff --git a/plugins/lark/scripts/tests/test_runtime.py b/plugins/lark/scripts/tests/test_runtime.py index 07a7051..fcf31cb 100644 --- a/plugins/lark/scripts/tests/test_runtime.py +++ b/plugins/lark/scripts/tests/test_runtime.py @@ -42,7 +42,12 @@ def test_identity_routing_is_explicit(self): lark_cli.identity(["im", "--as"]) def test_managed_commands_cannot_start_a_second_login(self): - for args in (["auth", "login"], ["config", "init"], ["profile", "switch"]): + for args in ( + ["auth", "login"], + ["config", "init"], + ["profile", "switch"], + ["event", "consume", "im.message.receive_v1"], + ): with self.assertRaises(AuthError): lark_cli.business_args(args) self.assertEqual(lark_cli.business_args(["auth", "status"]), ["account-status"]) diff --git a/plugins/lark/skills/lark-event/SKILL.md b/plugins/lark/skills/lark-event/SKILL.md index 39bed6f..f50989c 100644 --- a/plugins/lark/skills/lark-event/SKILL.md +++ b/plugins/lark/skills/lark-event/SKILL.md @@ -3,6 +3,8 @@ name: lark-event description: "飞书/Lark 实时事件监听/订阅/消费:通过 `lark-cli event consume EventKey` 以 NDJSON 流式输出事件(覆盖 IM 消息/表情/群聊变更、任务更新、视频会议开始/入会/结束、妙记生成、画板更新等)。适用于飞书机器人、实时消息处理、长时订阅者、流式 webhook/推送处理。支持 `--max-events` / `--timeout` 有界运行,以及 stderr ready-marker 约定——面向以子进程方式运行的 AI agent。" --- +> 事件订阅需要本地常驻总线,请在来源设备使用应用身份 `--as bot` 执行。云端仅支持 `event list` 和 `event schema`,不要在云端启动 consume、stop、status 或后台总线。 + ## Wegent 本地与云端运行 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 From 8a19a44fbb002d81f0f7a6fbc664977c5d583a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E4=BF=8A=E9=BE=99?= Date: Wed, 9 Sep 2026 18:32:42 +0800 Subject: [PATCH 4/4] docs(lark): align managed input examples with broker transport --- plugins/lark/skills/lark-approval/SKILL.md | 2 +- plugins/lark/skills/lark-attendance/SKILL.md | 2 +- plugins/lark/skills/lark-base/SKILL.md | 2 +- plugins/lark/skills/lark-calendar/SKILL.md | 2 +- plugins/lark/skills/lark-contact/SKILL.md | 2 +- plugins/lark/skills/lark-doc/SKILL.md | 2 +- plugins/lark/skills/lark-drive/SKILL.md | 2 +- plugins/lark/skills/lark-event/SKILL.md | 2 +- plugins/lark/skills/lark-im/SKILL.md | 2 +- plugins/lark/skills/lark-mail/SKILL.md | 2 +- plugins/lark/skills/lark-markdown/SKILL.md | 2 +- plugins/lark/skills/lark-minutes/SKILL.md | 2 +- plugins/lark/skills/lark-note/SKILL.md | 2 +- plugins/lark/skills/lark-okr/SKILL.md | 2 +- plugins/lark/skills/lark-openapi-explorer/SKILL.md | 2 +- plugins/lark/skills/lark-shared/SKILL.md | 4 ++-- plugins/lark/skills/lark-sheets/SKILL.md | 2 +- plugins/lark/skills/lark-skill-maker/SKILL.md | 2 +- plugins/lark/skills/lark-slides/SKILL.md | 2 +- plugins/lark/skills/lark-task/SKILL.md | 2 +- plugins/lark/skills/lark-vc-agent/SKILL.md | 2 +- plugins/lark/skills/lark-vc/SKILL.md | 2 +- plugins/lark/skills/lark-whiteboard/SKILL.md | 2 +- plugins/lark/skills/lark-wiki/SKILL.md | 2 +- plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md | 2 +- plugins/lark/skills/lark-workflow-standup-report/SKILL.md | 2 +- 26 files changed, 27 insertions(+), 27 deletions(-) diff --git a/plugins/lark/skills/lark-approval/SKILL.md b/plugins/lark/skills/lark-approval/SKILL.md index bc60e84..0a1edeb 100644 --- a/plugins/lark/skills/lark-approval/SKILL.md +++ b/plugins/lark/skills/lark-approval/SKILL.md @@ -7,7 +7,7 @@ description: "飞书审批:查询和处理审批待办/已办/实例,搜索 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 **CRITICAL — 开始前 MUST 先用 Read 工具读取 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md),其中包含认证、权限处理** diff --git a/plugins/lark/skills/lark-attendance/SKILL.md b/plugins/lark/skills/lark-attendance/SKILL.md index 9950c74..b7b90b1 100644 --- a/plugins/lark/skills/lark-attendance/SKILL.md +++ b/plugins/lark/skills/lark-attendance/SKILL.md @@ -7,7 +7,7 @@ description: "飞书考勤打卡:查询自己的考勤打卡记录" - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # attendance (v1) diff --git a/plugins/lark/skills/lark-base/SKILL.md b/plugins/lark/skills/lark-base/SKILL.md index 0bfbb20..fef7f5c 100644 --- a/plugins/lark/skills/lark-base/SKILL.md +++ b/plugins/lark/skills/lark-base/SKILL.md @@ -7,7 +7,7 @@ description: "飞书多维表格(Base)操作:建表、字段、记录、 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # base diff --git a/plugins/lark/skills/lark-calendar/SKILL.md b/plugins/lark/skills/lark-calendar/SKILL.md index 7e5e48f..0694e55 100644 --- a/plugins/lark/skills/lark-calendar/SKILL.md +++ b/plugins/lark/skills/lark-calendar/SKILL.md @@ -7,7 +7,7 @@ description: "飞书日历:管理日历日程和会议室。查看/搜索日 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # calendar (v4) diff --git a/plugins/lark/skills/lark-contact/SKILL.md b/plugins/lark/skills/lark-contact/SKILL.md index e9247a2..b79b9c6 100644 --- a/plugins/lark/skills/lark-contact/SKILL.md +++ b/plugins/lark/skills/lark-contact/SKILL.md @@ -7,7 +7,7 @@ description: "飞书 / Lark 通讯录:按姓名 / 邮箱解析成 open_id,或按 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 ## 选哪个命令 diff --git a/plugins/lark/skills/lark-doc/SKILL.md b/plugins/lark/skills/lark-doc/SKILL.md index af8c460..5a9091d 100644 --- a/plugins/lark/skills/lark-doc/SKILL.md +++ b/plugins/lark/skills/lark-doc/SKILL.md @@ -7,7 +7,7 @@ description: "飞书云文档(Docx / Wiki 文档):读取和编辑飞书文 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # docs diff --git a/plugins/lark/skills/lark-drive/SKILL.md b/plugins/lark/skills/lark-drive/SKILL.md index a7d66bc..7a00d5c 100644 --- a/plugins/lark/skills/lark-drive/SKILL.md +++ b/plugins/lark/skills/lark-drive/SKILL.md @@ -7,7 +7,7 @@ description: "飞书云空间(云盘/云存储):管理 Drive 文件和文 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # drive (v1) diff --git a/plugins/lark/skills/lark-event/SKILL.md b/plugins/lark/skills/lark-event/SKILL.md index f50989c..01c4c2a 100644 --- a/plugins/lark/skills/lark-event/SKILL.md +++ b/plugins/lark/skills/lark-event/SKILL.md @@ -9,7 +9,7 @@ description: "飞书/Lark 实时事件监听/订阅/消费:通过 `lark-cli ev - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # Lark Events diff --git a/plugins/lark/skills/lark-im/SKILL.md b/plugins/lark/skills/lark-im/SKILL.md index 9ee106a..0b7cc20 100644 --- a/plugins/lark/skills/lark-im/SKILL.md +++ b/plugins/lark/skills/lark-im/SKILL.md @@ -7,7 +7,7 @@ description: "飞书即时通讯:收发消息和管理群聊。发送和回复 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # im (v1) diff --git a/plugins/lark/skills/lark-mail/SKILL.md b/plugins/lark/skills/lark-mail/SKILL.md index f031e71..0b4cee3 100644 --- a/plugins/lark/skills/lark-mail/SKILL.md +++ b/plugins/lark/skills/lark-mail/SKILL.md @@ -7,7 +7,7 @@ description: "飞书邮箱:当用户提到起草邮件、写邮件、草稿、 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # mail (v1) diff --git a/plugins/lark/skills/lark-markdown/SKILL.md b/plugins/lark/skills/lark-markdown/SKILL.md index 1c5f0ba..635249e 100644 --- a/plugins/lark/skills/lark-markdown/SKILL.md +++ b/plugins/lark/skills/lark-markdown/SKILL.md @@ -7,7 +7,7 @@ description: "飞书 Markdown:查看、创建、上传、编辑和比较 Markd - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # markdown (v1) diff --git a/plugins/lark/skills/lark-minutes/SKILL.md b/plugins/lark/skills/lark-minutes/SKILL.md index cfaf7ba..b6b1593 100644 --- a/plugins/lark/skills/lark-minutes/SKILL.md +++ b/plugins/lark/skills/lark-minutes/SKILL.md @@ -7,7 +7,7 @@ description: "飞书妙记:搜索妙记、查看妙记基础信息、下载/ - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # minutes (v1) diff --git a/plugins/lark/skills/lark-note/SKILL.md b/plugins/lark/skills/lark-note/SKILL.md index dc7c9d2..22dfb6a 100644 --- a/plugins/lark/skills/lark-note/SKILL.md +++ b/plugins/lark/skills/lark-note/SKILL.md @@ -7,7 +7,7 @@ description: "飞书会议纪要(Note)直查:已知 note_id 时查询纪 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # note (v1) diff --git a/plugins/lark/skills/lark-okr/SKILL.md b/plugins/lark/skills/lark-okr/SKILL.md index f304fd5..ac6709d 100644 --- a/plugins/lark/skills/lark-okr/SKILL.md +++ b/plugins/lark/skills/lark-okr/SKILL.md @@ -7,7 +7,7 @@ description: "飞书 OKR:管理目标与关键结果。查看和编辑 OKR 周 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # okr (v2) diff --git a/plugins/lark/skills/lark-openapi-explorer/SKILL.md b/plugins/lark/skills/lark-openapi-explorer/SKILL.md index 608c084..b3ecdf7 100644 --- a/plugins/lark/skills/lark-openapi-explorer/SKILL.md +++ b/plugins/lark/skills/lark-openapi-explorer/SKILL.md @@ -7,7 +7,7 @@ description: "飞书/Lark 原生 OpenAPI 探索:从官方文档库中挖掘未 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # OpenAPI Explorer diff --git a/plugins/lark/skills/lark-shared/SKILL.md b/plugins/lark/skills/lark-shared/SKILL.md index 545a623..b97af21 100644 --- a/plugins/lark/skills/lark-shared/SKILL.md +++ b/plugins/lark/skills/lark-shared/SKILL.md @@ -7,7 +7,7 @@ description: "用于 lark-cli 的配置与授权任务:auth login/status/logou - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # lark-cli 共享规则 @@ -123,7 +123,7 @@ lark-cli update - **禁止输出密钥**(appSecret、accessToken)到终端明文。 - **写入/删除操作前必须确认用户意图**。 - 用 `--dry-run` 预览危险请求。 -- **文件路径只接受相对路径**:`--file`、`--output`、`--output-dir`、`@file` 等路径参数只接受 cwd 下的相对路径,传绝对路径会报 `unsafe file path`。数据输入(`@file`、大 JSON)优先用 stdin 传入,避免路径和转义问题。 +- **文件路径只接受相对路径**:`--file`、`--output`、`--output-dir`、`@file` 等路径参数只接受 cwd 下的相对路径,传绝对路径会报 `unsafe file path`。数据输入(大 JSON)先写入工作目录的相对文件,再用 `@file` 传入。托管调用不支持 stdin 正文。 ## 高风险操作的审批协议(exit 10) diff --git a/plugins/lark/skills/lark-sheets/SKILL.md b/plugins/lark/skills/lark-sheets/SKILL.md index 2ce8c64..4cc5383 100644 --- a/plugins/lark/skills/lark-sheets/SKILL.md +++ b/plugins/lark/skills/lark-sheets/SKILL.md @@ -7,7 +7,7 @@ description: "飞书电子表格:创建和操作电子表格。支持创建表 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # sheets diff --git a/plugins/lark/skills/lark-skill-maker/SKILL.md b/plugins/lark/skills/lark-skill-maker/SKILL.md index 84837ec..ae2eecd 100644 --- a/plugins/lark/skills/lark-skill-maker/SKILL.md +++ b/plugins/lark/skills/lark-skill-maker/SKILL.md @@ -7,7 +7,7 @@ description: "创建 lark-cli 的自定义 Skill。当用户需要把飞书 API - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # Skill Maker diff --git a/plugins/lark/skills/lark-slides/SKILL.md b/plugins/lark/skills/lark-slides/SKILL.md index 68dd06c..5ea9e3b 100644 --- a/plugins/lark/skills/lark-slides/SKILL.md +++ b/plugins/lark/skills/lark-slides/SKILL.md @@ -7,7 +7,7 @@ description: "飞书幻灯片:创建和编辑幻灯片。创建演示文稿、 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # slides (v1) diff --git a/plugins/lark/skills/lark-task/SKILL.md b/plugins/lark/skills/lark-task/SKILL.md index c32083a..89a5bca 100644 --- a/plugins/lark/skills/lark-task/SKILL.md +++ b/plugins/lark/skills/lark-task/SKILL.md @@ -7,7 +7,7 @@ description: "飞书任务:管理任务、清单和任务智能体。创建待 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # task (v2) diff --git a/plugins/lark/skills/lark-vc-agent/SKILL.md b/plugins/lark/skills/lark-vc-agent/SKILL.md index 149de94..ce5e186 100644 --- a/plugins/lark/skills/lark-vc-agent/SKILL.md +++ b/plugins/lark/skills/lark-vc-agent/SKILL.md @@ -7,7 +7,7 @@ description: "飞书视频会议会中能力:用于让应用机器人真实加 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # vc-agent (v1) diff --git a/plugins/lark/skills/lark-vc/SKILL.md b/plugins/lark/skills/lark-vc/SKILL.md index 831d826..e9c176b 100644 --- a/plugins/lark/skills/lark-vc/SKILL.md +++ b/plugins/lark/skills/lark-vc/SKILL.md @@ -7,7 +7,7 @@ description: "飞书视频会议:搜索历史会议记录、查询会议纪要 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # vc (v1) diff --git a/plugins/lark/skills/lark-whiteboard/SKILL.md b/plugins/lark/skills/lark-whiteboard/SKILL.md index 27d1778..579725c 100644 --- a/plugins/lark/skills/lark-whiteboard/SKILL.md +++ b/plugins/lark/skills/lark-whiteboard/SKILL.md @@ -9,7 +9,7 @@ description: > - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 > [!IMPORTANT] > - 运行 `lark-cli --version`,确认可用,无需询问用户。 diff --git a/plugins/lark/skills/lark-wiki/SKILL.md b/plugins/lark/skills/lark-wiki/SKILL.md index 16cc5ed..3f800bf 100644 --- a/plugins/lark/skills/lark-wiki/SKILL.md +++ b/plugins/lark/skills/lark-wiki/SKILL.md @@ -7,7 +7,7 @@ description: "飞书知识库:管理知识空间、空间成员和文档节点 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # wiki (v2) diff --git a/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md b/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md index 5573318..1f7fe1b 100644 --- a/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md +++ b/plugins/lark/skills/lark-workflow-meeting-summary/SKILL.md @@ -7,7 +7,7 @@ description: "会议纪要整理工作流:汇总指定时间范围内的会议 - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # 会议纪要汇总工作流 diff --git a/plugins/lark/skills/lark-workflow-standup-report/SKILL.md b/plugins/lark/skills/lark-workflow-standup-report/SKILL.md index 8b32c2a..9387986 100644 --- a/plugins/lark/skills/lark-workflow-standup-report/SKILL.md +++ b/plugins/lark/skills/lark-workflow-standup-report/SKILL.md @@ -7,7 +7,7 @@ description: "日程待办摘要:编排 calendar +agenda 和 task +get-my-task - 当前 `SKILL.md` 所在目录的 `../..` 是插件根目录。首次调用前,macOS/Linux 运行 `sh "<插件根目录>/scripts/ensure-lark-ready.sh"`;Windows 运行 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\ensure-lark-ready.ps1"`。 - 下文的 `lark-cli ...` 是逻辑命令。实际执行时,macOS/Linux 使用 `sh "<插件根目录>/scripts/run-lark-cli.sh" ...`;Windows 使用 `powershell -NoProfile -ExecutionPolicy Bypass -File "<插件根目录>\scripts\run-lark-cli.ps1" ...`。 -- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 +- 应用配置和首次用户 OAuth 在本机原连接入口完成;Wegent 在后台托管用户 OAuth(`lark`)及应用凭据(`lark-app`)。云端准备脚本只检查托管认证,不安装 CLI 或发起登录。实际命令必须经过上述包装器,不能直接调用裸 CLI、读取认证文件或索取 Token。用户调用默认 `--as user`,应用调用显式 `--as bot`,两种云端授权独立管理。托管请求正文通过参数或相对路径文件传入,不使用 stdin。托管状态失效或需要增量授权时,在本机原连接入口重新连接;不要在云端执行 auth/config/profile 命令。 # 日程待办摘要工作流