workbuddy-market 2.27.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. workbuddy_market-2.27.0/LICENSE +21 -0
  2. workbuddy_market-2.27.0/PKG-INFO +109 -0
  3. workbuddy_market-2.27.0/README.md +81 -0
  4. workbuddy_market-2.27.0/pyproject.toml +54 -0
  5. workbuddy_market-2.27.0/setup.cfg +4 -0
  6. workbuddy_market-2.27.0/src/workbuddy_market/__init__.py +12 -0
  7. workbuddy_market-2.27.0/src/workbuddy_market/__main__.py +6 -0
  8. workbuddy_market-2.27.0/src/workbuddy_market/adapters/__init__.py +6 -0
  9. workbuddy_market-2.27.0/src/workbuddy_market/adapters/workbuddy.py +301 -0
  10. workbuddy_market-2.27.0/src/workbuddy_market/application.py +492 -0
  11. workbuddy_market-2.27.0/src/workbuddy_market/artifact.py +347 -0
  12. workbuddy_market-2.27.0/src/workbuddy_market/catalog.py +242 -0
  13. workbuddy_market-2.27.0/src/workbuddy_market/cli.py +231 -0
  14. workbuddy_market-2.27.0/src/workbuddy_market/config.py +302 -0
  15. workbuddy_market-2.27.0/src/workbuddy_market/doctor.py +221 -0
  16. workbuddy_market-2.27.0/src/workbuddy_market/errors.py +38 -0
  17. workbuddy_market-2.27.0/src/workbuddy_market/favorites.py +79 -0
  18. workbuddy_market-2.27.0/src/workbuddy_market/fsutil.py +106 -0
  19. workbuddy_market-2.27.0/src/workbuddy_market/hasher.py +78 -0
  20. workbuddy_market-2.27.0/src/workbuddy_market/installer.py +323 -0
  21. workbuddy_market-2.27.0/src/workbuddy_market/locking.py +137 -0
  22. workbuddy_market-2.27.0/src/workbuddy_market/logging.py +143 -0
  23. workbuddy_market-2.27.0/src/workbuddy_market/onboard.py +337 -0
  24. workbuddy_market-2.27.0/src/workbuddy_market/ownership.py +104 -0
  25. workbuddy_market-2.27.0/src/workbuddy_market/packaging.py +543 -0
  26. workbuddy_market-2.27.0/src/workbuddy_market/paths.py +120 -0
  27. workbuddy_market-2.27.0/src/workbuddy_market/registry.py +402 -0
  28. workbuddy_market-2.27.0/src/workbuddy_market/scanner.py +215 -0
  29. workbuddy_market-2.27.0/src/workbuddy_market/state.py +189 -0
  30. workbuddy_market-2.27.0/src/workbuddy_market/sync.py +92 -0
  31. workbuddy_market-2.27.0/src/workbuddy_market/transactions.py +347 -0
  32. workbuddy_market-2.27.0/src/workbuddy_market/trash.py +498 -0
  33. workbuddy_market-2.27.0/src/workbuddy_market/uninstaller.py +242 -0
  34. workbuddy_market-2.27.0/src/workbuddy_market/version.py +10 -0
  35. workbuddy_market-2.27.0/src/workbuddy_market.egg-info/PKG-INFO +109 -0
  36. workbuddy_market-2.27.0/src/workbuddy_market.egg-info/SOURCES.txt +38 -0
  37. workbuddy_market-2.27.0/src/workbuddy_market.egg-info/dependency_links.txt +1 -0
  38. workbuddy_market-2.27.0/src/workbuddy_market.egg-info/entry_points.txt +2 -0
  39. workbuddy_market-2.27.0/src/workbuddy_market.egg-info/requires.txt +3 -0
  40. workbuddy_market-2.27.0/src/workbuddy_market.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zjs105910
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: workbuddy-market
3
+ Version: 2.27.0
4
+ Summary: Local-first plugin marketplace for WorkBuddy: package local skills, register into WorkBuddy, browse & install from a 127.0.0.1-only web UI
5
+ Author: zjs105910
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/zjs105910/workbuddy-market
8
+ Project-URL: Repository, https://github.com/zjs105910/workbuddy-market
9
+ Project-URL: Documentation, https://github.com/zjs105910/workbuddy-market/blob/main/ADVANCED_GUIDE.md
10
+ Project-URL: Issues, https://github.com/zjs105910/workbuddy-market/issues
11
+ Project-URL: Security, https://github.com/zjs105910/workbuddy-market/blob/main/SECURITY.md
12
+ Project-URL: Changelog, https://github.com/zjs105910/workbuddy-market/blob/main/CHANGELOG.md
13
+ Keywords: workbuddy,skill,plugin,marketplace,agent-skills,ai-agents,developer-tools,local-first
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Environment :: Web Environment
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # WorkBuddy Market
30
+
31
+ **简体中文** · [English](README_EN.md)
32
+
33
+ [![CI](https://github.com/zjs105910/workbuddy-market/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/zjs105910/workbuddy-market/actions/workflows/ci.yml)
34
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](pyproject.toml)
35
+ [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
36
+
37
+ **给 WorkBuddy 用的本机优先插件市场。** 将本机 Skills 打包成插件,在精选社区目录或 GitHub 中查找技能;安装前查看来源、权限声明和兼容性,再通过哈希校验与可恢复事务完成安装、更新和卸载。
38
+
39
+ *A local-first plugin marketplace for WorkBuddy: package local Skills, explore community and GitHub sources, review declared permissions and compatibility, and manage installs through hash verification and recoverable transactions.*
40
+
41
+ ## 30 秒了解它
42
+
43
+ - **管理自己的 Skills**:将本机技能打包、同步,并注册到 WorkBuddy 插件面板。
44
+ - **发现社区技能**:浏览精选目录,也可以搜索 GitHub 上的公开仓库。
45
+ - **安装过程更可追溯**:安装前有来源与风险预览;支持内容校验、事务恢复和回收站。
46
+ - **本机优先**:网页服务只监听 `127.0.0.1`;你的私有市场配置和本机技能内容不随仓库提交。
47
+
48
+ ## 快速上手(Windows)
49
+
50
+ 要求:Python 3.10+、Git。想先体验网页,不改动 WorkBuddy 配置,推荐从安全模式开始。
51
+
52
+ ```powershell
53
+ git clone https://github.com/zjs105910/workbuddy-market.git
54
+ cd workbuddy-market
55
+ .\安全模式-只开网页.cmd
56
+ ```
57
+
58
+ 启动后打开本机地址(默认 `http://127.0.0.1:8777/`)。**请保持启动窗口打开**;结束时回到窗口按 `Ctrl+C`。
59
+
60
+ 已经准备好要接入 WorkBuddy 时,再运行 `一键启动.cmd`。首次初始化的交互向导中,**直接回车是只浏览**;只有明确输入 `2` 才会请求注册到 WorkBuddy。注册前仍会进行打包与自检前置检查。
61
+
62
+ 不会使用命令行?克隆仓库后,也可以在资源管理器中双击 `安全模式-只开网页.cmd`。想了解其他启动参数、插件配置或 CLI,请看[详细使用与技术参考](ADVANCED_GUIDE.md)。
63
+
64
+ ## 看看界面
65
+
66
+ | 本机技能 | 精选市场 |
67
+ | --- | --- |
68
+ | ![本机技能管理界面](docs/screenshots/home-local.png) | ![精选社区插件界面](docs/screenshots/home-curated.png) |
69
+
70
+ 安装前的来源、权限与兼容性预览:
71
+
72
+ ![安装前详情预览](docs/screenshots/detail-four-questions.png)
73
+
74
+ > 截图由项目脚本使用合成数据生成。实际可用操作会根据本机环境与插件来源有所不同。
75
+
76
+ ## 产品演示与正式版本
77
+
78
+ - [可复现演示:录制方法、中文字幕与隐私边界](docs/demo-video.md)
79
+ - [正式 Release 发布流程](docs/release-process.md)
80
+ - [观看当前演示构建(约 80 秒,MP4 + 中文字幕)](https://github.com/zjs105910/workbuddy-market/actions/runs/38121812213)(Actions 附件有效期 30 天)。
81
+ - 正式版本的 MP4、字幕、Python 安装包与 SHA-256 校验文件将在 [GitHub Releases](https://github.com/zjs105910/workbuddy-market/releases) 中一起发布。当前演示构建是临时附件;`registry-artifacts-*` 是社区插件产物,不是市场本体安装包。
82
+
83
+ ## 安装前请知道
84
+
85
+ - 卸载时会检查插件是否由本市场管理、内容是否被修改;不属于本市场管理的技能不会被随意删除。回收站与事务恢复用于降低误操作影响。
86
+ - **权限声明和风险预览不是运行时沙箱。** 第三方 Skill 仍可能执行其自身逻辑;安装前请核查来源与内容,不要仅凭“精选”标签就盲目信任。
87
+ - 首次只想体验网页:使用 `安全模式-只开网页.cmd`。想撤销 WorkBuddy 中的市场注册:使用 `撤销注册.cmd`。这与卸载已经安装的技能是不同操作。
88
+
89
+ ## 平台支持矩阵(验证状态)
90
+
91
+ | 平台 | 当前状态 |
92
+ | --- | --- |
93
+ | Windows 10/11 | 主要使用与验证环境;仓库文档记录了 WorkBuddy 5.7.6 面板实测 |
94
+ | Linux | 有 CI 与部分自动化检查;尚未验证真实 WorkBuddy 宿主集成 |
95
+ | macOS | 尚未验证 |
96
+
97
+ ## 文档与社区
98
+
99
+ | 你想做什么 | 入口 |
100
+ | --- | --- |
101
+ | 配置市场、使用命令行、了解安全边界 | [详细使用与技术参考](ADVANCED_GUIDE.md) |
102
+ | 了解首次启动、收录和恢复问题 | [FAQ](docs/FAQ.md) |
103
+ | 了解插件包协议与校验 | [插件包协议](docs/plugin-spec.md) |
104
+ | 报告安全问题 | [安全策略](SECURITY.md) |
105
+ | 提交代码或改进文档 | [贡献指南](CONTRIBUTING.md) |
106
+ | 查看版本变化 | [CHANGELOG](CHANGELOG.md) |
107
+ | 提交 Bug / 功能建议 | [GitHub Issues](https://github.com/zjs105910/workbuddy-market/issues) |
108
+
109
+ 欢迎 Star、提 Issue、提交可复现的问题与 PR。项目采用 MIT License。
@@ -0,0 +1,81 @@
1
+ # WorkBuddy Market
2
+
3
+ **简体中文** · [English](README_EN.md)
4
+
5
+ [![CI](https://github.com/zjs105910/workbuddy-market/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/zjs105910/workbuddy-market/actions/workflows/ci.yml)
6
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-blue.svg)](pyproject.toml)
7
+ [![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
8
+
9
+ **给 WorkBuddy 用的本机优先插件市场。** 将本机 Skills 打包成插件,在精选社区目录或 GitHub 中查找技能;安装前查看来源、权限声明和兼容性,再通过哈希校验与可恢复事务完成安装、更新和卸载。
10
+
11
+ *A local-first plugin marketplace for WorkBuddy: package local Skills, explore community and GitHub sources, review declared permissions and compatibility, and manage installs through hash verification and recoverable transactions.*
12
+
13
+ ## 30 秒了解它
14
+
15
+ - **管理自己的 Skills**:将本机技能打包、同步,并注册到 WorkBuddy 插件面板。
16
+ - **发现社区技能**:浏览精选目录,也可以搜索 GitHub 上的公开仓库。
17
+ - **安装过程更可追溯**:安装前有来源与风险预览;支持内容校验、事务恢复和回收站。
18
+ - **本机优先**:网页服务只监听 `127.0.0.1`;你的私有市场配置和本机技能内容不随仓库提交。
19
+
20
+ ## 快速上手(Windows)
21
+
22
+ 要求:Python 3.10+、Git。想先体验网页,不改动 WorkBuddy 配置,推荐从安全模式开始。
23
+
24
+ ```powershell
25
+ git clone https://github.com/zjs105910/workbuddy-market.git
26
+ cd workbuddy-market
27
+ .\安全模式-只开网页.cmd
28
+ ```
29
+
30
+ 启动后打开本机地址(默认 `http://127.0.0.1:8777/`)。**请保持启动窗口打开**;结束时回到窗口按 `Ctrl+C`。
31
+
32
+ 已经准备好要接入 WorkBuddy 时,再运行 `一键启动.cmd`。首次初始化的交互向导中,**直接回车是只浏览**;只有明确输入 `2` 才会请求注册到 WorkBuddy。注册前仍会进行打包与自检前置检查。
33
+
34
+ 不会使用命令行?克隆仓库后,也可以在资源管理器中双击 `安全模式-只开网页.cmd`。想了解其他启动参数、插件配置或 CLI,请看[详细使用与技术参考](ADVANCED_GUIDE.md)。
35
+
36
+ ## 看看界面
37
+
38
+ | 本机技能 | 精选市场 |
39
+ | --- | --- |
40
+ | ![本机技能管理界面](docs/screenshots/home-local.png) | ![精选社区插件界面](docs/screenshots/home-curated.png) |
41
+
42
+ 安装前的来源、权限与兼容性预览:
43
+
44
+ ![安装前详情预览](docs/screenshots/detail-four-questions.png)
45
+
46
+ > 截图由项目脚本使用合成数据生成。实际可用操作会根据本机环境与插件来源有所不同。
47
+
48
+ ## 产品演示与正式版本
49
+
50
+ - [可复现演示:录制方法、中文字幕与隐私边界](docs/demo-video.md)
51
+ - [正式 Release 发布流程](docs/release-process.md)
52
+ - [观看当前演示构建(约 80 秒,MP4 + 中文字幕)](https://github.com/zjs105910/workbuddy-market/actions/runs/38121812213)(Actions 附件有效期 30 天)。
53
+ - 正式版本的 MP4、字幕、Python 安装包与 SHA-256 校验文件将在 [GitHub Releases](https://github.com/zjs105910/workbuddy-market/releases) 中一起发布。当前演示构建是临时附件;`registry-artifacts-*` 是社区插件产物,不是市场本体安装包。
54
+
55
+ ## 安装前请知道
56
+
57
+ - 卸载时会检查插件是否由本市场管理、内容是否被修改;不属于本市场管理的技能不会被随意删除。回收站与事务恢复用于降低误操作影响。
58
+ - **权限声明和风险预览不是运行时沙箱。** 第三方 Skill 仍可能执行其自身逻辑;安装前请核查来源与内容,不要仅凭“精选”标签就盲目信任。
59
+ - 首次只想体验网页:使用 `安全模式-只开网页.cmd`。想撤销 WorkBuddy 中的市场注册:使用 `撤销注册.cmd`。这与卸载已经安装的技能是不同操作。
60
+
61
+ ## 平台支持矩阵(验证状态)
62
+
63
+ | 平台 | 当前状态 |
64
+ | --- | --- |
65
+ | Windows 10/11 | 主要使用与验证环境;仓库文档记录了 WorkBuddy 5.7.6 面板实测 |
66
+ | Linux | 有 CI 与部分自动化检查;尚未验证真实 WorkBuddy 宿主集成 |
67
+ | macOS | 尚未验证 |
68
+
69
+ ## 文档与社区
70
+
71
+ | 你想做什么 | 入口 |
72
+ | --- | --- |
73
+ | 配置市场、使用命令行、了解安全边界 | [详细使用与技术参考](ADVANCED_GUIDE.md) |
74
+ | 了解首次启动、收录和恢复问题 | [FAQ](docs/FAQ.md) |
75
+ | 了解插件包协议与校验 | [插件包协议](docs/plugin-spec.md) |
76
+ | 报告安全问题 | [安全策略](SECURITY.md) |
77
+ | 提交代码或改进文档 | [贡献指南](CONTRIBUTING.md) |
78
+ | 查看版本变化 | [CHANGELOG](CHANGELOG.md) |
79
+ | 提交 Bug / 功能建议 | [GitHub Issues](https://github.com/zjs105910/workbuddy-market/issues) |
80
+
81
+ 欢迎 Star、提 Issue、提交可复现的问题与 PR。项目采用 MIT License。
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "workbuddy-market"
7
+ description = "Local-first plugin marketplace for WorkBuddy: package local skills, register into WorkBuddy, browse & install from a 127.0.0.1-only web UI"
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "zjs105910" }]
12
+ keywords = ["workbuddy", "skill", "plugin", "marketplace", "agent-skills", "ai-agents", "developer-tools", "local-first"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Environment :: Web Environment",
17
+ "Operating System :: Microsoft :: Windows",
18
+ # POSIX 的真实 WorkBuddy 宿主集成仍未验证。CI 覆盖 POSIX 语义冒烟与 pytest;
19
+ # 完整隔离 selftest 硬门槛暂只在 Windows 跑(平台状态见 README / ADVANCED_GUIDE)。
20
+ "Operating System :: POSIX :: Linux",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.13",
24
+ ]
25
+ # 版本号唯一来源仍是 src/workbuddy_market/version.py(约定不变),
26
+ # 这里动态读取,避免出现第二个要同步的常量。
27
+ dynamic = ["version"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/zjs105910/workbuddy-market"
31
+ Repository = "https://github.com/zjs105910/workbuddy-market"
32
+ Documentation = "https://github.com/zjs105910/workbuddy-market/blob/main/ADVANCED_GUIDE.md"
33
+ Issues = "https://github.com/zjs105910/workbuddy-market/issues"
34
+ Security = "https://github.com/zjs105910/workbuddy-market/blob/main/SECURITY.md"
35
+ Changelog = "https://github.com/zjs105910/workbuddy-market/blob/main/CHANGELOG.md"
36
+
37
+ [project.scripts]
38
+ # 高级用户入口(在仓库 clone 目录内运行,操作当前 clone 的市场);
39
+ # Windows 新手入口仍是一键启动.cmd —— 两条路一个内核。
40
+ workbuddy-market = "workbuddy_market.cli:main"
41
+
42
+ [project.optional-dependencies]
43
+ # 开发者测试框架(v3-roadmap P1:pytest 承担日常回归,selftest 保留为
44
+ # 零依赖一键环境诊断)。`pip install -e .[dev]` 或 CI 里单装 pytest。
45
+ dev = ["pytest>=8.0"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+
50
+ [tool.setuptools.dynamic]
51
+ version = { attr = "workbuddy_market.version.MARKET_VERSION" }
52
+
53
+ [tool.setuptools.packages.find]
54
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """WorkBuddy 本机插件市场 —— 内核包。
2
+
3
+ v2.8(R2)起,market_core.py 的实现按 docs/开源重构方案.md §3.1 陆续迁入本包:
4
+
5
+ * R2(v2.8):paths / errors / fsutil / hasher / locking / logging(无状态纯函数)
6
+ * R3(v2.9):config / scanner / sync / version
7
+ * R4(v2.12):trash / ownership / transactions(注入点经 core 晚绑定保持语义)
8
+ * R5+(后续轮次):installer / uninstaller / state / diagnostics / cli / api
9
+
10
+ 兼容入口仍是仓库根的 ``market_core.py``:它 re-export 本包的全部公开符号,
11
+ ``import market_core`` 与 ``core.X`` 的用法(含 selftest 的属性注入)不受影响。
12
+ """
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ """python -m workbuddy_market —— 与 workbuddy-market 命令行同一入口。"""
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ """adapters —— 宿主适配层(v2.17 新增)。
3
+
4
+ 每个子模块封装「对某一个宿主 / 外部系统内部格式的全部知识」。
5
+ 内核与其余模块只经 adapter 触碰宿主;宿主格式变化只改这一层。
6
+ """
@@ -0,0 +1,301 @@
1
+ # -*- coding: utf-8 -*-
2
+ """adapters.workbuddy —— WorkBuddy 宿主适配层(v2.17 新增,R6 半程)。
3
+
4
+ 这个包从 v1 起就知道很多 WorkBuddy 的内部细节:known_marketplaces.json
5
+ 的形状、marketplace 条目字段、备份目录约定…… v2.16 之前这些知识散落在
6
+ market_core 的注册段里。宿主程序未来最容易变化的就是这些东西
7
+ (WorkBuddy 5.x / 6.x 的市场协议、安装位置、条目格式),所以现在把
8
+ **所有「WorkBuddy 宿主格式」的读写收进这一层**,内核只经它触碰宿主。
9
+
10
+ 职责(全部自 market_core.py 逐字迁入,v2.17):
11
+
12
+ · known_marketplaces.json 的读取(含 strict 口径:坏了宁可报错也不覆盖);
13
+ · 备份(纳秒戳)与轮转;
14
+ · 条目构造(directory 型市场);
15
+ · 乐观合并写入(_commit_known,与 WorkBuddy 自身写入竞争的防线);
16
+ · register / unregister 编排;
17
+ · capabilities():宿主能力矩阵(未来版本探测的落点);
18
+ · known_health():known 文件的健康检查(deep_check 消费)。
19
+
20
+ 注入点纪律(R4/R5 延续):register() 依赖 core 的 _sync_packaging(注入点),
21
+ 在调用点晚绑定 ``import market_core``。本模块内部的互调(register →
22
+ _commit_known → _read_known_or_die)走**本模块命名空间** —— selftest 的
23
+ patch 落点随迁 ``workbuddy_market.adapters.workbuddy``(与 R4 把
24
+ ownership/trash 的 patch 落点迁到包内同一先例)。core 侧名字全部
25
+ re-export,``core.register`` / ``core._read_known_or_die`` 仍是同一对象,
26
+ 但 **patch 语义随迁**:要拦截注册链路里的读,请 patch 本模块。
27
+
28
+ 未来 WorkBuddy 6.x 若改了市场格式,改这一个文件就够了 —— 这就是
29
+ adapter 的意义。
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import os
35
+ import shutil
36
+ import time
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+
40
+ from ..config import ConfigError, load_config
41
+ from ..fsutil import atomic_write_text, read_json
42
+ from ..locking import locked
43
+ from ..logging import log
44
+ from ..paths import BACKUP_DIR, BACKUP_KEEP, KNOWN_PATH, MARKET_ROOT, WB
45
+
46
+ WORKBUDDY_ADAPTER_VERSION = 1
47
+
48
+ # 宿主能力矩阵:未来探测宿主版本后按版本收紧/放开。
49
+ # 现在如实标注当前已实现/已验证的能力(v2.17 口径)。
50
+ _CAPABILITIES = {
51
+ "marketplace_register": True, # 写 known_marketplaces.json
52
+ "marketplace_unregister": True,
53
+ "directory_market": True, # directory 型市场源
54
+ "skill_install_location": True, # 直写 ~/.workbuddy/skills
55
+ "security_scan": False, # 宿主侧安装前扫描:未验证,不虚报
56
+ "skill_enable": False, # 宿主侧启用/禁用开关:未实现
57
+ "auto_update": False, # 宿主自动更新本市场:显式关闭
58
+ }
59
+
60
+
61
+ def capabilities() -> dict:
62
+ """宿主能力矩阵(诚实口径:没验证过的能力一律 False,不虚报)。"""
63
+ return dict(_CAPABILITIES)
64
+
65
+
66
+ def detect_host_version(host_dir: Path | None = None) -> str | None:
67
+ """探测宿主 WorkBuddy 版本(v2.20,诚实口径:找不到就 None)。
68
+
69
+ 来源(按序,全部是**显式**来源):
70
+
71
+ 1. 环境变量 ``WORKBUDDY_VERSION``(宿主 / 测试显式注入);
72
+ 2. ``<宿主目录>/last-launch.json`` 的 ``version`` 字段 —— WorkBuddy
73
+ 每次启动自己写的启动记录(实测 5.7.6 存在)。属**不可信状态
74
+ 文件**:形状不对、不是字符串、不含数字 → 当没有。
75
+
76
+ 绝不去猜安装目录、注册表或进程参数 —— 探测不到就返回 None,
77
+ 调用方(verify_package / compatibility_report)如实标「宿主版本
78
+ 未知」,绝不把「不知道」说成「满足」或「不满足」。
79
+ """
80
+ env = (os.environ.get("WORKBUDDY_VERSION") or "").strip()
81
+ if env:
82
+ return env[:32]
83
+ d = Path(host_dir) if host_dir is not None else WB
84
+ data = read_json(d / "last-launch.json", None)
85
+ if not isinstance(data, dict):
86
+ return None
87
+ v = data.get("version")
88
+ if not isinstance(v, str) or not v.strip() or not any(c.isdigit() for c in v):
89
+ return None
90
+ return v.strip()[:32]
91
+
92
+
93
+ # ---------------------------------------------------------------- known 读取
94
+
95
+ def read_known(strict: bool = False) -> dict:
96
+ """读 known_marketplaces.json。
97
+
98
+ strict=True 时,文件存在但读不出来会**抛 ConfigError**,而不是当成空表。
99
+ 这一点很关键:如果当成空表,register() 就会拿一个只含自己一条的记录
100
+ 去覆盖原文件 —— 用户其它几个市场(包括 WorkBuddy 自带的)会被直接抹掉。
101
+ 宁可报错让用户去 .backups 里恢复,也不能替用户把数据删了。
102
+ """
103
+ data = read_json(KNOWN_PATH, None, strict=strict)
104
+ if data is None:
105
+ return {}
106
+ if not isinstance(data, dict):
107
+ if strict:
108
+ raise ConfigError(f"{KNOWN_PATH.name} 的顶层必须是一个对象,实际是 {type(data).__name__}")
109
+ return {}
110
+ return data
111
+
112
+
113
+ def read_known_or_die() -> dict:
114
+ """写操作前读 —— 文件坏了就拒绝继续,绝不覆盖。"""
115
+ if not KNOWN_PATH.is_file():
116
+ return {}
117
+ try:
118
+ return read_known(strict=True)
119
+ except ConfigError as exc:
120
+ raise RuntimeError(
121
+ f"拒绝覆盖:{KNOWN_PATH.name} 已损坏,读不出来。\n"
122
+ f" 原因:{exc}\n"
123
+ f" 文件:{KNOWN_PATH}\n"
124
+ f" 备份:{BACKUP_DIR}(挑一份改名成 known_marketplaces.json 即可恢复)\n"
125
+ " 修好或删掉它再重试。"
126
+ ) from exc
127
+
128
+
129
+ def backup_known() -> Path | None:
130
+ """备份 known_marketplaces.json。
131
+
132
+ 时间戳带纳秒,避免同一秒内两次备份互相覆盖
133
+ (v1 用秒级戳 + 「已存在就返回」会让第二次修改没有备份)。
134
+ """
135
+ if not KNOWN_PATH.is_file():
136
+ return None
137
+ BACKUP_DIR.mkdir(parents=True, exist_ok=True)
138
+ stamp = time.strftime("%Y%m%d-%H%M%S") + f"-{time.time_ns() % 1_000_000_000:09d}"
139
+ dst = BACKUP_DIR / f"known_marketplaces.{stamp}.json"
140
+ shutil.copy2(KNOWN_PATH, dst)
141
+ log("info", "backup", f"已备份 known_marketplaces.json → {dst.name}")
142
+ return dst
143
+
144
+
145
+ def _prune_backups(keep: int = BACKUP_KEEP) -> None:
146
+ if not BACKUP_DIR.is_dir():
147
+ return
148
+ items = sorted(BACKUP_DIR.glob("known_marketplaces.*.json"))
149
+ if len(items) <= keep:
150
+ return
151
+ for old in items[: len(items) - keep]:
152
+ try:
153
+ old.unlink()
154
+ except OSError:
155
+ pass
156
+
157
+
158
+ def is_registered(cfg: dict | None = None) -> bool:
159
+ cfg = cfg or load_config()
160
+ return cfg.get("marketId") in read_known()
161
+
162
+
163
+ def entry_for(cfg: dict) -> dict:
164
+ """本市场在 known_marketplaces.json 里的条目(directory 型)。"""
165
+ return {
166
+ "manifestName": cfg.get("marketId", "wb-local-market"),
167
+ "type": "directory",
168
+ "source": {"source": "directory", "path": str(MARKET_ROOT)},
169
+ "installLocation": str(MARKET_ROOT),
170
+ "description": cfg.get("description", ""),
171
+ "lastUpdated": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
172
+ "autoUpdate": False,
173
+ "isBuiltIn": False,
174
+ }
175
+
176
+
177
+ def commit_known(mutate, *, attempts: int = 4, what: str = "修改"):
178
+ """对 known_marketplaces.json 做乐观合并写入。
179
+
180
+ WorkBuddy 自己也会改这个文件(autoUpdate 市场刷新 lastUpdated、zip 地址
181
+ 换成带内容哈希的版本),而它**不抢我们这把锁**。原来的
182
+ 「读 → 改 → 原子写 → 读回校验」只能发现「整个条目消失」,
183
+ 发现不了「某个市场的字段在中间被别人改了、又被我们整份覆盖回去」。
184
+
185
+ 改成:落笔之前再读一次,确认和我们读到的快照一模一样才写;
186
+ 不一样就基于最新内容重新合并。这把竞争窗口从「整个读-改-写过程」
187
+ 压到了「重读到 replace 之间的几十微秒」。
188
+
189
+ 返回 (before, candidate)。mutate 返回 False 表示无需改动(提前收工)。
190
+ """
191
+ for attempt in range(attempts):
192
+ before = read_known_or_die()
193
+ candidate = mutate(dict(before))
194
+ if candidate is False:
195
+ return before, None
196
+ latest = read_known_or_die()
197
+ if latest != before:
198
+ log("warn", "register",
199
+ f"{what}前发现 {KNOWN_PATH.name} 已被外部改动"
200
+ f"(第 {attempt + 1} 次),基于最新内容重新合并")
201
+ continue
202
+ backup_known()
203
+ atomic_write_text(KNOWN_PATH,
204
+ json.dumps(candidate, ensure_ascii=False, indent=2) + "\n",
205
+ durable=True)
206
+ _prune_backups()
207
+ after = read_known()
208
+ lost = set(before) - set(after)
209
+ if lost:
210
+ log("warn", "register",
211
+ f"检测到写回期间有市场消失:{', '.join(sorted(lost))}(WorkBuddy 可能同时在写)")
212
+ return before, after
213
+ raise RuntimeError(
214
+ f"{what}失败:{KNOWN_PATH.name} 被反复改写({attempts} 次都撞车)。\n"
215
+ "如果 WorkBuddy 正在运行,稍后再试一次即可;文件没有被写坏。"
216
+ )
217
+
218
+
219
+ # ---------------------------------------------------------------- 注册 / 撤销
220
+
221
+ def register(force: bool = False) -> bool:
222
+ """把本市场写进 known_marketplaces.json。幂等;返回是否发生了改动。
223
+
224
+ WorkBuddy 运行时会自己改写这个文件,所以并发保护有两层:
225
+ · 全程持文件锁 → 防本工具自己的多个进程互相覆盖(lost update)
226
+ · 乐观合并(读 → 改 → 重读 → 比对 → 写)→ 缩小与 WorkBuddy 自身写入的竞争窗口
227
+ """
228
+ import market_core as _core # noqa: PLC0415 —— _sync_packaging 注入点晚绑定
229
+ with locked():
230
+ cfg = load_config()
231
+ mid = cfg.get("marketId", "wb-local-market")
232
+
233
+ if not _core.MANIFEST_PATH.is_file():
234
+ _core._sync_packaging(quiet=True)
235
+
236
+ entry = entry_for(cfg)
237
+
238
+ # 比较时忽略 lastUpdated,否则每次启动都会重写一遍这个文件
239
+ def _same(a: dict, b: dict) -> bool:
240
+ return ({k: v for k, v in a.items() if k != "lastUpdated"}
241
+ == {k: v for k, v in b.items() if k != "lastUpdated"})
242
+
243
+ def _mutate(known: dict):
244
+ if not force and isinstance(known.get(mid), dict) and _same(known[mid], entry):
245
+ return False
246
+ known[mid] = entry
247
+ return known
248
+
249
+ before, after = commit_known(_mutate, what="注册")
250
+ if after is None:
251
+ log("info", "register", "已注册且条目未变,跳过")
252
+ return False
253
+ if mid not in after:
254
+ log("warn", "register", "写回后本市场条目不见了 —— WorkBuddy 可能同时改写了该文件")
255
+ raise RuntimeError("写回校验失败:注册条目未生效(可能和 WorkBuddy 的自动更新撞车,重跑一次即可)")
256
+ log("info", "register", f"已注册市场 {mid} → {MARKET_ROOT}")
257
+ return True
258
+
259
+
260
+ def unregister() -> bool:
261
+ with locked():
262
+ cfg = load_config()
263
+ mid = cfg.get("marketId", "wb-local-market")
264
+
265
+ def _mutate(known: dict):
266
+ if mid not in known:
267
+ return False
268
+ del known[mid]
269
+ return known
270
+
271
+ before, after = commit_known(_mutate, what="撤销注册")
272
+ if after is None:
273
+ log("info", "unregister", "本来就没注册,跳过")
274
+ return False
275
+ log("info", "unregister", f"已撤销注册 {mid}")
276
+ return True
277
+
278
+
279
+ # ---------------------------------------------------------------- 体检
280
+
281
+ def known_health() -> str | None:
282
+ """known_marketplaces.json 的健康检查。正常返回 None;有问题返回人话。
283
+
284
+ deep_check(core)消费它 —— 宿主格式的问题由 adapter 自己诊断,
285
+ 内核不再直接解读这个文件的形状。
286
+ """
287
+ if not KNOWN_PATH.parent.is_dir():
288
+ return f"WorkBuddy 插件目录不存在:{KNOWN_PATH.parent}"
289
+ if KNOWN_PATH.is_file():
290
+ try:
291
+ read_json(KNOWN_PATH, None, strict=True)
292
+ except ConfigError as exc:
293
+ return f"known_marketplaces.json 不可用:{exc}"
294
+ return None
295
+
296
+
297
+ # v2.17 之前的旧名字(market_core / 第三方老脚本可能引用):同一对象。
298
+ _read_known = read_known
299
+ _read_known_or_die = read_known_or_die
300
+ _entry_for = entry_for
301
+ _commit_known = commit_known