feat(event): add personal event subscriptions (#589)

* dws event

* fix consume fail

* test: add stream ticket injection probe

* feat: add portal ticket stream mode

* user event

* fix: allow portal ticket normal without app secret

* event

* user event

* eventType filter

* refactor(event): 优化IPC端点路径处理和改进相关测试

- 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑
- 新增event包实现Unix socket路径长度限制及长路径fallback机制
- 添加endpoint_test.go覆盖路径短长及唯一性的单元测试
- 修改busctl模块使用统一的IPC端点获取方法,避免重复实现
- transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误
- 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度
- 去除多个文件中无用的runtime导入,简化代码依赖

* opt

* event skill

* default value

* install script event

* fix: remove subscribe id event fanout filter

* fix(personal): 修正指定发送人消息描述错误

* more im event

* filter subId

* fix: align personal event schema with stream payload

* fix: avoid duplicate app helper name

* feat: simplify personal event schemas

* feat: simplify event schema output

* docs: refine dingtalk event skill references

* feat: align personal event consume flags

* fix event stop and status visibility

* hide app event public entrypoints

* hide incomplete personal sender event

* remove external event reference comments

* chore(event): prepare official release

* fix(event): harden personal stream lifecycle

---------

Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com>
Co-authored-by: zhengyubai <zhengyubai618@gmail.com>
This commit is contained in:
xianfeng wang
2026-07-10 17:54:43 +08:00
committed by GitHub
co-authored by 玉澜 zhengyubai
parent ea6fd16d11
commit aa6abc5ed6
114 changed files with 18987 additions and 182 deletions
+380
View File
@@ -1,9 +1,13 @@
package scripts_test
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
@@ -163,6 +167,7 @@ func TestInstallScriptsUseGitHubReleaseSkillsAsset(t *testing.T) {
for _, rel := range []string{
filepath.Join("..", "..", "scripts", "install.sh"),
filepath.Join("..", "..", "scripts", "install-event.sh"),
filepath.Join("..", "..", "scripts", "install.ps1"),
filepath.Join("..", "..", "scripts", "install-skills.sh"),
} {
@@ -186,6 +191,290 @@ func TestInstallScriptsUseGitHubReleaseSkillsAsset(t *testing.T) {
}
}
func TestInstallEventScriptStaticExpectations(t *testing.T) {
t.Parallel()
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh"))
if err != nil {
t.Fatalf("Abs(install-event.sh) error = %v", err)
}
data, err := os.ReadFile(scriptPath)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v", scriptPath, err)
}
text := string(data)
for _, want := range []string{
"DingTalk-Real-AI/dingtalk-workspace-cli",
"releases/latest",
"EVENT_VERSION",
"DWS_SKILLS_ONLY",
"dingtalk-event",
"user_im_message_receive_o2o",
".config/opencode/skills",
"$HOME/.dws/skills/multi/$EVENT_SKILL_NAME",
"$HOME/.dws/skills/mono",
} {
if !strings.Contains(text, want) {
t.Fatalf("install-event.sh missing %q", want)
}
}
for _, avoid := range []string{
"releases?per_page=30",
"select(.tag_name",
"dingtalk-dev",
"client-secret",
"--as app",
} {
if strings.Contains(text, avoid) {
t.Fatalf("install-event.sh should not expose old app/dev install content %q", avoid)
}
}
}
func TestInstallEventScriptInstallsBinaryAndEventSkills(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("POSIX shell installer test is for unix-like hosts")
}
root := t.TempDir()
fakeHome := filepath.Join(root, "home")
installDir := filepath.Join(root, "bin")
releaseDir := filepath.Join(root, "release")
stubRoot := filepath.Join(root, "stubs")
assetName := "dws-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz"
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
t.Skipf("unsupported test arch %s", runtime.GOARCH)
}
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
t.Skipf("unsupported test os %s", runtime.GOOS)
}
if err := os.MkdirAll(releaseDir, 0o755); err != nil {
t.Fatalf("MkdirAll(%s) error = %v", releaseDir, err)
}
writeTarGz(t, filepath.Join(releaseDir, assetName), map[string]string{
"dws": "fake-event-binary\n",
})
writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{
"multi/dingtalk-event/SKILL.md": "event skill user_im_message_receive_o2o\n",
"mono/SKILL.md": "mono skill user_im_message_receive_o2o\n",
"SKILL.md": "legacy mono root\n",
})
writeFakeCurl(t, filepath.Join(stubRoot, "curl"))
for _, dir := range []string{
filepath.Join(fakeHome, ".codex"),
filepath.Join(fakeHome, ".config", "opencode"),
} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll(%s) error = %v", dir, err)
}
}
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh"))
if err != nil {
t.Fatalf("Abs(install-event.sh) error = %v", err)
}
cmd := exec.Command("sh", scriptPath)
cmd.Env = append(os.Environ(),
"HOME="+fakeHome,
"PATH="+stubRoot+":"+os.Getenv("PATH"),
"EVENT_VERSION=v1.0.51",
"DWS_INSTALL_DIR="+installDir,
"FAKE_RELEASE_DIR="+releaseDir,
"FAKE_ASSET_NAME="+assetName,
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install-event.sh error = %v\noutput:\n%s", err, string(output))
}
got := string(output)
for _, want := range []string{
"Version: v1.0.51",
"Skill dingtalk-event",
"Skill dws",
"dws event consume user_im_message_receive_o2o",
} {
if !strings.Contains(got, want) {
t.Fatalf("install-event output missing %q:\n%s", want, got)
}
}
binaryData, err := os.ReadFile(filepath.Join(installDir, "dws"))
if err != nil {
t.Fatalf("ReadFile(installed dws) error = %v", err)
}
if string(binaryData) != "fake-event-binary\n" {
t.Fatalf("installed binary content = %q", string(binaryData))
}
for _, rel := range []string{
".agents/skills/dingtalk-event/SKILL.md",
".codex/skills/dingtalk-event/SKILL.md",
".config/opencode/skills/dingtalk-event/SKILL.md",
".agents/skills/dws/SKILL.md",
".codex/skills/dws/SKILL.md",
".dws/skills/multi/dingtalk-event/SKILL.md",
".dws/skills/mono/SKILL.md",
} {
p := filepath.Join(fakeHome, filepath.FromSlash(rel))
data, err := os.ReadFile(p)
if err != nil {
t.Fatalf("ReadFile(%s) error = %v\noutput:\n%s", p, err, got)
}
if !strings.Contains(string(data), "user_im_message_receive_o2o") {
t.Fatalf("%s does not contain event skill marker: %q", p, string(data))
}
}
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-dev")); !os.IsNotExist(err) {
t.Fatalf("dingtalk-dev should not be installed by install-event.sh, stat err=%v", err)
}
}
func TestInstallEventScriptSkillsOnlySkipsBinary(t *testing.T) {
t.Parallel()
root := t.TempDir()
fakeHome := filepath.Join(root, "home")
installDir := filepath.Join(root, "bin")
releaseDir := filepath.Join(root, "release")
stubRoot := filepath.Join(root, "stubs")
if err := os.MkdirAll(releaseDir, 0o755); err != nil {
t.Fatalf("MkdirAll(%s) error = %v", releaseDir, err)
}
writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{
"multi/dingtalk-event/SKILL.md": "event skill user_im_message_receive_o2o\n",
"mono/SKILL.md": "mono skill user_im_message_receive_o2o\n",
})
writeFakeCurl(t, filepath.Join(stubRoot, "curl"))
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh"))
if err != nil {
t.Fatalf("Abs(install-event.sh) error = %v", err)
}
cmd := exec.Command("sh", scriptPath)
cmd.Env = append(os.Environ(),
"HOME="+fakeHome,
"PATH="+stubRoot+":"+os.Getenv("PATH"),
"EVENT_VERSION=v1.0.51",
"DWS_INSTALL_DIR="+installDir,
"DWS_SKILLS_ONLY=1",
"FAKE_RELEASE_DIR="+releaseDir,
"FAKE_ASSET_NAME=unused",
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install-event.sh skills-only error = %v\noutput:\n%s", err, string(output))
}
if _, err := os.Stat(filepath.Join(installDir, "dws")); !os.IsNotExist(err) {
t.Fatalf("DWS_SKILLS_ONLY=1 should not install binary, stat err=%v\noutput:\n%s", err, string(output))
}
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event", "SKILL.md")); err != nil {
t.Fatalf("skills-only should install event skill: %v\noutput:\n%s", err, string(output))
}
}
func TestInstallEventScriptNoSkillsOnlyInstallsBinary(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("POSIX shell installer test is for unix-like hosts")
}
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
t.Skipf("unsupported test arch %s", runtime.GOARCH)
}
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
t.Skipf("unsupported test os %s", runtime.GOOS)
}
root := t.TempDir()
fakeHome := filepath.Join(root, "home")
installDir := filepath.Join(root, "bin")
releaseDir := filepath.Join(root, "release")
stubRoot := filepath.Join(root, "stubs")
assetName := "dws-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz"
writeTarGz(t, filepath.Join(releaseDir, assetName), map[string]string{
"dws": "fake-event-binary\n",
})
writeFakeCurl(t, filepath.Join(stubRoot, "curl"))
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh"))
if err != nil {
t.Fatalf("Abs(install-event.sh) error = %v", err)
}
cmd := exec.Command("sh", scriptPath)
cmd.Env = append(os.Environ(),
"HOME="+fakeHome,
"PATH="+stubRoot+":"+os.Getenv("PATH"),
"EVENT_VERSION=v1.0.51",
"DWS_INSTALL_DIR="+installDir,
"DWS_NO_SKILLS=1",
"FAKE_RELEASE_DIR="+releaseDir,
"FAKE_ASSET_NAME="+assetName,
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install-event.sh no-skills error = %v\noutput:\n%s", err, string(output))
}
if _, err := os.Stat(filepath.Join(installDir, "dws")); err != nil {
t.Fatalf("DWS_NO_SKILLS=1 should install binary: %v\noutput:\n%s", err, string(output))
}
if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event")); !os.IsNotExist(err) {
t.Fatalf("DWS_NO_SKILLS=1 should not install event skill, stat err=%v\noutput:\n%s", err, string(output))
}
}
func TestInstallEventScriptDefaultsToLatestStableRelease(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("POSIX shell installer test is for unix-like hosts")
}
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
t.Skipf("unsupported test arch %s", runtime.GOARCH)
}
if runtime.GOOS != "darwin" && runtime.GOOS != "linux" {
t.Skipf("unsupported test os %s", runtime.GOOS)
}
root := t.TempDir()
fakeHome := filepath.Join(root, "home")
installDir := filepath.Join(root, "bin")
releaseDir := filepath.Join(root, "release")
stubRoot := filepath.Join(root, "stubs")
assetName := "dws-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz"
writeTarGz(t, filepath.Join(releaseDir, assetName), map[string]string{
"dws": "fake-event-binary\n",
})
writeFakeCurl(t, filepath.Join(stubRoot, "curl"))
writeFakeGH(t, filepath.Join(stubRoot, "gh"), "v1.0.51")
scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh"))
if err != nil {
t.Fatalf("Abs(install-event.sh) error = %v", err)
}
cmd := exec.Command("sh", scriptPath)
cmd.Env = append(os.Environ(),
"HOME="+fakeHome,
"PATH="+stubRoot+":"+os.Getenv("PATH"),
"DWS_INSTALL_DIR="+installDir,
"DWS_NO_SKILLS=1",
"FAKE_RELEASE_DIR="+releaseDir,
"FAKE_ASSET_NAME="+assetName,
)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install-event.sh latest release error = %v\noutput:\n%s", err, string(output))
}
if !strings.Contains(string(output), "Version: v1.0.51") {
t.Fatalf("install-event.sh did not resolve the latest stable version:\n%s", string(output))
}
}
func TestInstallScriptsUseFlattenedSkillsSourceRoot(t *testing.T) {
t.Parallel()
@@ -442,6 +731,97 @@ done
}
}
func writeTarGz(t *testing.T, path string, files map[string]string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll(%s) error = %v", filepath.Dir(path), err)
}
f, err := os.Create(path)
if err != nil {
t.Fatalf("Create(%s) error = %v", path, err)
}
gz := gzip.NewWriter(f)
tw := tar.NewWriter(gz)
for name, content := range files {
hdr := &tar.Header{
Name: name,
Mode: 0o755,
Size: int64(len(content)),
}
if err := tw.WriteHeader(hdr); err != nil {
t.Fatalf("WriteHeader(%s) error = %v", name, err)
}
if _, err := tw.Write([]byte(content)); err != nil {
t.Fatalf("Write(%s) error = %v", name, err)
}
}
if err := tw.Close(); err != nil {
t.Fatalf("tar Close error = %v", err)
}
if err := gz.Close(); err != nil {
t.Fatalf("gzip Close error = %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("Close(%s) error = %v", path, err)
}
}
func writeZip(t *testing.T, path string, files map[string]string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll(%s) error = %v", filepath.Dir(path), err)
}
f, err := os.Create(path)
if err != nil {
t.Fatalf("Create(%s) error = %v", path, err)
}
zw := zip.NewWriter(f)
for name, content := range files {
w, err := zw.Create(name)
if err != nil {
t.Fatalf("zip Create(%s) error = %v", name, err)
}
if _, err := w.Write([]byte(content)); err != nil {
t.Fatalf("zip Write(%s) error = %v", name, err)
}
}
if err := zw.Close(); err != nil {
t.Fatalf("zip Close error = %v", err)
}
if err := f.Close(); err != nil {
t.Fatalf("Close(%s) error = %v", path, err)
}
}
func writeFakeCurl(t *testing.T, path string) {
t.Helper()
const script = `#!/bin/sh
set -eu
out=""
url=""
while [ "$#" -gt 0 ]; do
case "$1" in
-o) out="$2"; shift 2 ;;
-*) shift ;;
*) url="$1"; shift ;;
esac
done
[ -n "$out" ] || { echo "fake curl: missing -o" >&2; exit 1; }
case "$url" in
*"/${FAKE_ASSET_NAME}") cp "$FAKE_RELEASE_DIR/$FAKE_ASSET_NAME" "$out" ;;
*"/dws-skills.zip") cp "$FAKE_RELEASE_DIR/dws-skills.zip" "$out" ;;
*) echo "fake curl: unexpected URL $url" >&2; exit 1 ;;
esac
`
mustWriteFile(t, path, []byte(script), 0o755)
}
func writeFakeGH(t *testing.T, path, version string) {
t.Helper()
script := "#!/bin/sh\nprintf '%s\\n' '" + version + "'\n"
mustWriteFile(t, path, []byte(script), 0o755)
}
func mustWriteFile(t *testing.T, path string, data []byte, mode os.FileMode) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {