feat: DSH编排基建首版(server/tests 88用例/P2插件/8020端口/PORT-NOTE/MOBILE_APP)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# 团队默认配置(team-defaults)
|
||||
|
||||
把团队的模型/供应商/界面偏好打包成一个可分发快照。新成员装上后一条命令
|
||||
就能让自己的 harness 配置与服务端团队设置对齐,不用手工抄。
|
||||
|
||||
## 用法
|
||||
|
||||
```bash
|
||||
# 下载并安装插件
|
||||
python client/dsh_sync_client.py install team-defaults
|
||||
|
||||
# 应用到本地配置(已存在的键不会被覆盖)
|
||||
python ~/.dsh/plugins/team-defaults/files/apply.py --config ~/.dsh/config.json
|
||||
|
||||
# 先看看会改什么
|
||||
python .../apply.py --config ~/.dsh/config.json --dry-run
|
||||
|
||||
# 只应用模型相关
|
||||
python .../apply.py --config ~/.dsh/config.json --only 'model.*' 'provider.*'
|
||||
|
||||
# 强制以团队值为准(覆盖本地改动)
|
||||
python .../apply.py --config ~/.dsh/config.json --overwrite
|
||||
```
|
||||
|
||||
写入前会自动在同目录留一份 `config.json.bak`。
|
||||
|
||||
## 更新快照
|
||||
|
||||
`files/defaults.json` 可以手工维护,也可以从服务端导出:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer <token>" http://<server>:8020/v1/settings \
|
||||
| python -c "import json,sys; d=json.load(sys.stdin)['settings']; \
|
||||
print(json.dumps({k: v['value'] for k,v in d.items()}, indent=2, ensure_ascii=False))" \
|
||||
> files/defaults.json
|
||||
```
|
||||
|
||||
改完 `manifest.json` 里的 `version`,再发布:
|
||||
|
||||
```bash
|
||||
bash tools/publish-plugin.sh team-defaults
|
||||
```
|
||||
|
||||
## 安全
|
||||
|
||||
插件里不含任何 API Key。`provider.base_url` 只是端点地址;真实密钥按
|
||||
ARCHITECTURE.md §7 留在各设备本地或走团队网关。
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply this plugin's settings snapshot to a harness config file.
|
||||
|
||||
python apply.py --config ~/.dsh/config.json # merge defaults in
|
||||
python apply.py --config ~/.dsh/config.json --dry-run
|
||||
python apply.py --config ~/.dsh/config.json --only model.* provider.*
|
||||
|
||||
Existing keys are kept unless --overwrite is given, so applying this to a
|
||||
machine someone has already tuned will not stomp their choices.
|
||||
|
||||
A backup of the original file is written next to it before any change.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULTS = Path(__file__).resolve().parent / "defaults.json"
|
||||
|
||||
|
||||
def flatten(prefix: str, value, out: dict) -> None:
|
||||
"""`{"model": {"temperature": 0.7}}` -> `{"model.temperature": 0.7}`."""
|
||||
if isinstance(value, dict):
|
||||
for k, v in value.items():
|
||||
flatten(f"{prefix}.{k}" if prefix else k, v, out)
|
||||
else:
|
||||
out[prefix] = value
|
||||
|
||||
|
||||
def load_defaults() -> dict:
|
||||
raw = json.loads(DEFAULTS.read_text(encoding="utf-8"))
|
||||
out: dict = {}
|
||||
flatten("", raw, out)
|
||||
return {k: v for k, v in out.items() if not k.startswith("_")}
|
||||
|
||||
|
||||
def set_path(cfg: dict, dotted: str, value) -> None:
|
||||
parts = dotted.split(".")
|
||||
node = cfg
|
||||
for p in parts[:-1]:
|
||||
nxt = node.get(p)
|
||||
if not isinstance(nxt, dict):
|
||||
nxt = {}
|
||||
node[p] = nxt
|
||||
node = nxt
|
||||
node[parts[-1]] = value
|
||||
|
||||
|
||||
def get_path(cfg: dict, dotted: str):
|
||||
node = cfg
|
||||
for p in dotted.split("."):
|
||||
if not isinstance(node, dict) or p not in node:
|
||||
return None, False
|
||||
node = node[p]
|
||||
return node, True
|
||||
|
||||
|
||||
def apply(config_path: Path, *, overwrite: bool, dry_run: bool,
|
||||
only: list[str] | None) -> dict:
|
||||
defaults = load_defaults()
|
||||
if only:
|
||||
defaults = {k: v for k, v in defaults.items()
|
||||
if any(fnmatch.fnmatch(k, pat) for pat in only)}
|
||||
|
||||
if config_path.is_file():
|
||||
cfg = json.loads(config_path.read_text(encoding="utf-8") or "{}")
|
||||
else:
|
||||
cfg = {}
|
||||
|
||||
added, kept = [], []
|
||||
for key, value in sorted(defaults.items()):
|
||||
_cur, present = get_path(cfg, key)
|
||||
if present and not overwrite:
|
||||
kept.append(key)
|
||||
continue
|
||||
set_path(cfg, key, value)
|
||||
added.append(key)
|
||||
|
||||
if added and not dry_run:
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if config_path.is_file():
|
||||
shutil.copy2(config_path, config_path.with_suffix(config_path.suffix + ".bak"))
|
||||
config_path.write_text(
|
||||
json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
||||
|
||||
return {"added": added, "kept": kept, "config": str(config_path)}
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
ap = argparse.ArgumentParser(description="apply team default settings")
|
||||
ap.add_argument("--config", type=Path, required=True)
|
||||
ap.add_argument("--overwrite", action="store_true",
|
||||
help="replace values that already exist")
|
||||
ap.add_argument("--dry-run", action="store_true")
|
||||
ap.add_argument("--only", nargs="*", metavar="GLOB",
|
||||
help="limit to keys matching these patterns, e.g. 'model.*'")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
result = apply(args.config, overwrite=args.overwrite, dry_run=args.dry_run,
|
||||
only=args.only)
|
||||
if args.dry_run:
|
||||
print("dry run — nothing written")
|
||||
print(f"config : {result['config']}")
|
||||
print(f"added : {len(result['added'])}" + (f" {result['added']}" if result["added"] else ""))
|
||||
print(f"kept : {len(result['kept'])}" + (f" {result['kept']}" if result["kept"] else ""))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"_comment": "由 publish-plugin.sh 从服务端 settings 导出,或手工维护",
|
||||
"model.default": "deepseek-chat",
|
||||
"model.temperature": 0.7,
|
||||
"model.max_tokens": 4096,
|
||||
"provider.base_url": "https://api.deepseek.com",
|
||||
"provider.timeout_seconds": 120,
|
||||
"provider.max_retries": 3,
|
||||
"harness.plugin_channel": "stable",
|
||||
"ui.theme": "dark",
|
||||
"ui.language": "zh-CN"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"id": "team-defaults",
|
||||
"version": "1.0.0",
|
||||
"name": "团队默认配置",
|
||||
"description": "把当前团队设置固化成一个可分发快照,新成员的 harness 装上即用",
|
||||
"channel": "stable",
|
||||
"min_harness_version": "0.0.0",
|
||||
"entry": "files/apply.py",
|
||||
"files": ["files/apply.py", "files/defaults.json", "README.md"]
|
||||
}
|
||||
Reference in New Issue
Block a user