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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/lark-package.yml
Original file line number Diff line number Diff line change
@@ -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
67 changes: 62 additions & 5 deletions plugins/lark/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lark",
"version": "0.1.4",
"description": "通过本机已完成认证的官方 CLI,使用飞书/Lark 消息、文档、多维表格、电子表格、日历、任务、会议与企业协作能力。",
"version": "0.2.0",
"description": "通过飞书官方 CLI 使用协作能力,本机登录后由 Wegent 托管用户 OAuth 与应用机器人认证。",
"author": {
"name": "Wegent"
},
Expand All @@ -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": [
Expand All @@ -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"
}
}
}
}
]
}
7 changes: 7 additions & 0 deletions plugins/lark/.wework-build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"schemaVersion": 1,
"entrypoint": ".wework-build/lark-auth/package.py",
"outputs": [
"scripts/native"
]
}
148 changes: 148 additions & 0 deletions plugins/lark/.wework-build/lark-auth/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""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(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) }",
"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",
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


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=6))
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()
Loading
Loading