git-ai-control 0.2.0
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.
- package/CHANGELOG.md +41 -0
- package/COMPATIBILITY.md +66 -0
- package/README.md +180 -0
- package/bin/git-ai-control.js +57 -0
- package/docs/images/git-ai-settings.jpg +0 -0
- package/docs/images/plugin-management.jpg +0 -0
- package/launchagents/com.git-ai.control-panel.plist.in +24 -0
- package/launchagents/com.git-ai.skill-usage-filter.plist.in +22 -0
- package/package.json +45 -0
- package/plugins/upload-filter/plugin_filter_runtime.py +560 -0
- package/policy.example.json +53 -0
- package/scripts/install.sh +241 -0
- package/scripts/uninstall.sh +91 -0
- package/server.py +534 -0
- package/static/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/static/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/static/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/static/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/static/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/static/assets/index-DWeFCLPQ.js +50 -0
- package/static/assets/index-WWn7U1pB.css +2 -0
- package/static/index.html +14 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
|
5
|
+
GIT_AI_ROOT=${GIT_AI_ROOT:-"${HOME}/.git-ai"}
|
|
6
|
+
LAUNCH_AGENTS_ROOT="${HOME}/Library/LaunchAgents"
|
|
7
|
+
CONTROL_PANEL_DIR="${GIT_AI_ROOT}/control-panel"
|
|
8
|
+
FILTER_DIR="${GIT_AI_ROOT}/filters"
|
|
9
|
+
GIT_AI_BINARY="${GIT_AI_ROOT}/bin/git-ai"
|
|
10
|
+
POLICY_PATH="${GIT_AI_ROOT}/filter_plugins.json"
|
|
11
|
+
METRICS_PATH="${GIT_AI_ROOT}/custom_metrics.json"
|
|
12
|
+
UPSTREAMS_PATH="${GIT_AI_ROOT}/upstream_metrics.json"
|
|
13
|
+
CUSTOM_METRICS_MARKER="${CONTROL_PANEL_DIR}/.custom-metrics-managed"
|
|
14
|
+
CONTROL_PANEL_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.control-panel.plist"
|
|
15
|
+
FILTER_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.skill-usage-filter.plist"
|
|
16
|
+
CONTROL_PANEL_LABEL="gui/$(id -u)/com.git-ai.control-panel"
|
|
17
|
+
FILTER_LABEL="gui/$(id -u)/com.git-ai.skill-usage-filter"
|
|
18
|
+
|
|
19
|
+
require_file() {
|
|
20
|
+
if [ ! -f "$1" ]; then
|
|
21
|
+
echo "缺少安装文件:$1" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
wait_for_port_release() {
|
|
27
|
+
port=$1
|
|
28
|
+
attempt=0
|
|
29
|
+
while [ "${attempt}" -lt 20 ]; do
|
|
30
|
+
if ! /usr/sbin/lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
attempt=$((attempt + 1))
|
|
34
|
+
sleep 0.25
|
|
35
|
+
done
|
|
36
|
+
echo "端口 ${port} 仍被占用,停止安装" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
wait_for_http() {
|
|
41
|
+
url=$1
|
|
42
|
+
label=$2
|
|
43
|
+
attempt=0
|
|
44
|
+
while [ "${attempt}" -lt 20 ]; do
|
|
45
|
+
if curl -fsS "${url}" >/dev/null 2>&1; then
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
attempt=$((attempt + 1))
|
|
49
|
+
sleep 0.5
|
|
50
|
+
done
|
|
51
|
+
echo "${label} 启动失败:${url}" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
require_file "${PROJECT_ROOT}/server.py"
|
|
56
|
+
require_file "${PROJECT_ROOT}/static/index.html"
|
|
57
|
+
require_file "${PROJECT_ROOT}/plugins/upload-filter/plugin_filter_runtime.py"
|
|
58
|
+
require_file "${PROJECT_ROOT}/policy.example.json"
|
|
59
|
+
require_file "${PROJECT_ROOT}/launchagents/com.git-ai.control-panel.plist.in"
|
|
60
|
+
require_file "${PROJECT_ROOT}/launchagents/com.git-ai.skill-usage-filter.plist.in"
|
|
61
|
+
require_file "${GIT_AI_BINARY}"
|
|
62
|
+
|
|
63
|
+
mkdir -p "${CONTROL_PANEL_DIR}" "${FILTER_DIR}" "${LAUNCH_AGENTS_ROOT}"
|
|
64
|
+
|
|
65
|
+
if [ ! -f "${POLICY_PATH}" ]; then
|
|
66
|
+
install -m 600 "${PROJECT_ROOT}/policy.example.json" "${POLICY_PATH}"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if /usr/bin/strings "${GIT_AI_BINARY}" | grep -Fq "custom_metrics.json"; then
|
|
70
|
+
/usr/bin/python3 - \
|
|
71
|
+
"${METRICS_PATH}" \
|
|
72
|
+
"${UPSTREAMS_PATH}" \
|
|
73
|
+
"${FILTER_DIR}/plugin_filter_runtime.py" <<'PY'
|
|
74
|
+
import ast
|
|
75
|
+
import json
|
|
76
|
+
import os
|
|
77
|
+
import sys
|
|
78
|
+
import tempfile
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
from urllib.parse import urlsplit
|
|
81
|
+
|
|
82
|
+
path = Path(sys.argv[1])
|
|
83
|
+
upstreams_path = Path(sys.argv[2])
|
|
84
|
+
installed_filter_path = Path(sys.argv[3])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_external_http_url(value):
|
|
88
|
+
if not isinstance(value, str):
|
|
89
|
+
return False
|
|
90
|
+
parsed = urlsplit(value)
|
|
91
|
+
return (
|
|
92
|
+
parsed.scheme in {"http", "https"}
|
|
93
|
+
and bool(parsed.netloc)
|
|
94
|
+
and parsed.hostname not in {"127.0.0.1", "localhost", "::1"}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def write_json(destination, payload):
|
|
99
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
101
|
+
prefix=f".{destination.name}.",
|
|
102
|
+
suffix=".tmp",
|
|
103
|
+
dir=destination.parent,
|
|
104
|
+
)
|
|
105
|
+
try:
|
|
106
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
107
|
+
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
108
|
+
handle.write("\n")
|
|
109
|
+
handle.flush()
|
|
110
|
+
os.fsync(handle.fileno())
|
|
111
|
+
os.chmod(temporary_name, 0o600)
|
|
112
|
+
os.replace(temporary_name, destination)
|
|
113
|
+
except Exception:
|
|
114
|
+
try:
|
|
115
|
+
os.unlink(temporary_name)
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
pass
|
|
118
|
+
raise
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
config = json.loads(path.read_text(encoding="utf-8"))
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
config = {}
|
|
125
|
+
|
|
126
|
+
if upstreams_path.exists():
|
|
127
|
+
saved_upstreams = json.loads(upstreams_path.read_text(encoding="utf-8"))
|
|
128
|
+
saved_routes = saved_upstreams.get("routes", saved_upstreams)
|
|
129
|
+
if not isinstance(saved_routes, dict) or not any(
|
|
130
|
+
is_external_http_url(url) for url in saved_routes.values()
|
|
131
|
+
):
|
|
132
|
+
raise SystemExit("原上报端点备份无效,已停止安装")
|
|
133
|
+
os.chmod(upstreams_path, 0o600)
|
|
134
|
+
else:
|
|
135
|
+
route_keys = {
|
|
136
|
+
"/legacy/commit": "commit_endpoint",
|
|
137
|
+
"/legacy/checkpoint": "checkpoint_endpoint",
|
|
138
|
+
"/legacy/token-usage": "token_usage_endpoint",
|
|
139
|
+
"/prompt-duration": "prompt_duration_endpoint",
|
|
140
|
+
"/commit": "commit_endpoint_v2",
|
|
141
|
+
"/checkpoint": "checkpoint_endpoint_v2",
|
|
142
|
+
"/token-usage": "token_usage_endpoint_v2",
|
|
143
|
+
"/skill-usage": "skill_usage_endpoint_v2",
|
|
144
|
+
"/agent-usage": "agent_usage_endpoint_v2",
|
|
145
|
+
"/prompt-report": "prompt_report_endpoint_v2",
|
|
146
|
+
}
|
|
147
|
+
routes = {
|
|
148
|
+
route: config[key]
|
|
149
|
+
for route, key in route_keys.items()
|
|
150
|
+
if is_external_http_url(config.get(key))
|
|
151
|
+
}
|
|
152
|
+
token_url = routes.get("/token-usage")
|
|
153
|
+
if token_url:
|
|
154
|
+
routes["/token-usage/batch"] = f"{token_url.rstrip('/')}/batch"
|
|
155
|
+
routes["/token-usage/stats"] = f"{token_url.rstrip('/')}/stats"
|
|
156
|
+
|
|
157
|
+
if not routes and installed_filter_path.exists():
|
|
158
|
+
try:
|
|
159
|
+
module = ast.parse(installed_filter_path.read_text(encoding="utf-8"))
|
|
160
|
+
for statement in module.body:
|
|
161
|
+
if not isinstance(statement, ast.Assign):
|
|
162
|
+
continue
|
|
163
|
+
if not any(
|
|
164
|
+
isinstance(target, ast.Name) and target.id == "UPSTREAMS"
|
|
165
|
+
for target in statement.targets
|
|
166
|
+
):
|
|
167
|
+
continue
|
|
168
|
+
legacy_routes = ast.literal_eval(statement.value)
|
|
169
|
+
routes = {
|
|
170
|
+
route: url
|
|
171
|
+
for route, url in legacy_routes.items()
|
|
172
|
+
if is_external_http_url(url)
|
|
173
|
+
}
|
|
174
|
+
break
|
|
175
|
+
except (OSError, SyntaxError, ValueError):
|
|
176
|
+
routes = {}
|
|
177
|
+
|
|
178
|
+
if not routes:
|
|
179
|
+
raise SystemExit(
|
|
180
|
+
"无法从 custom_metrics.json 识别原上报端点;"
|
|
181
|
+
"为避免丢失或回环请求,已停止安装"
|
|
182
|
+
)
|
|
183
|
+
write_json(upstreams_path, {"version": 1, "routes": routes})
|
|
184
|
+
|
|
185
|
+
config.update(
|
|
186
|
+
{
|
|
187
|
+
"enable": True,
|
|
188
|
+
"commit_endpoint": "http://127.0.0.1:38741/legacy/commit",
|
|
189
|
+
"checkpoint_endpoint": "http://127.0.0.1:38741/legacy/checkpoint",
|
|
190
|
+
"token_usage_endpoint": "http://127.0.0.1:38741/legacy/token-usage",
|
|
191
|
+
"prompt_duration_endpoint": "http://127.0.0.1:38741/prompt-duration",
|
|
192
|
+
"commit_endpoint_v2": "http://127.0.0.1:38741/commit",
|
|
193
|
+
"checkpoint_endpoint_v2": "http://127.0.0.1:38741/checkpoint",
|
|
194
|
+
"token_usage_endpoint_v2": "http://127.0.0.1:38741/token-usage",
|
|
195
|
+
"skill_usage_endpoint_v2": "http://127.0.0.1:38741/skill-usage",
|
|
196
|
+
"agent_usage_endpoint_v2": "http://127.0.0.1:38741/agent-usage",
|
|
197
|
+
"prompt_report_endpoint_v2": "http://127.0.0.1:38741/prompt-report",
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
write_json(path, config)
|
|
202
|
+
PY
|
|
203
|
+
touch "${CUSTOM_METRICS_MARKER}"
|
|
204
|
+
echo "检测到 custom_metrics 定制版:已启用细粒度上报过滤"
|
|
205
|
+
else
|
|
206
|
+
rm -f "${CUSTOM_METRICS_MARKER}"
|
|
207
|
+
echo "检测到官方上游版:保留原生配置管理,不修改无效的 custom_metrics.json"
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
install -m 700 "${PROJECT_ROOT}/server.py" "${CONTROL_PANEL_DIR}/server.py"
|
|
211
|
+
install -m 700 \
|
|
212
|
+
"${PROJECT_ROOT}/plugins/upload-filter/plugin_filter_runtime.py" \
|
|
213
|
+
"${FILTER_DIR}/plugin_filter_runtime.py"
|
|
214
|
+
/usr/bin/rsync -a --delete "${PROJECT_ROOT}/static/" "${CONTROL_PANEL_DIR}/static/"
|
|
215
|
+
|
|
216
|
+
sed "s#__GIT_AI_ROOT__#${GIT_AI_ROOT}#g" \
|
|
217
|
+
"${PROJECT_ROOT}/launchagents/com.git-ai.control-panel.plist.in" \
|
|
218
|
+
> "${CONTROL_PANEL_PLIST}"
|
|
219
|
+
sed "s#__GIT_AI_ROOT__#${GIT_AI_ROOT}#g" \
|
|
220
|
+
"${PROJECT_ROOT}/launchagents/com.git-ai.skill-usage-filter.plist.in" \
|
|
221
|
+
> "${FILTER_PLIST}"
|
|
222
|
+
chmod 600 "${CONTROL_PANEL_PLIST}" "${FILTER_PLIST}"
|
|
223
|
+
|
|
224
|
+
plutil -lint "${CONTROL_PANEL_PLIST}" "${FILTER_PLIST}"
|
|
225
|
+
/usr/bin/python3 -m py_compile \
|
|
226
|
+
"${CONTROL_PANEL_DIR}/server.py" \
|
|
227
|
+
"${FILTER_DIR}/plugin_filter_runtime.py"
|
|
228
|
+
|
|
229
|
+
launchctl bootout "${CONTROL_PANEL_LABEL}" >/dev/null 2>&1 || true
|
|
230
|
+
launchctl bootout "${FILTER_LABEL}" >/dev/null 2>&1 || true
|
|
231
|
+
wait_for_port_release 38741
|
|
232
|
+
wait_for_port_release 38742
|
|
233
|
+
launchctl bootstrap "gui/$(id -u)" "${FILTER_PLIST}"
|
|
234
|
+
launchctl bootstrap "gui/$(id -u)" "${CONTROL_PANEL_PLIST}"
|
|
235
|
+
wait_for_http "http://127.0.0.1:38741/health" "过滤服务"
|
|
236
|
+
wait_for_http "http://127.0.0.1:38742/api/status" "配置页面"
|
|
237
|
+
|
|
238
|
+
echo "安装完成:"
|
|
239
|
+
echo " 配置页面:http://127.0.0.1:38742"
|
|
240
|
+
echo " 过滤服务:http://127.0.0.1:38741/health"
|
|
241
|
+
echo " 用户配置:${GIT_AI_ROOT}"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
GIT_AI_ROOT=${GIT_AI_ROOT:-"${HOME}/.git-ai"}
|
|
5
|
+
LAUNCH_AGENTS_ROOT="${HOME}/Library/LaunchAgents"
|
|
6
|
+
CONTROL_PANEL_DIR="${GIT_AI_ROOT}/control-panel"
|
|
7
|
+
FILTER_SCRIPT="${GIT_AI_ROOT}/filters/plugin_filter_runtime.py"
|
|
8
|
+
METRICS_PATH="${GIT_AI_ROOT}/custom_metrics.json"
|
|
9
|
+
UPSTREAMS_PATH="${GIT_AI_ROOT}/upstream_metrics.json"
|
|
10
|
+
CUSTOM_METRICS_MARKER="${CONTROL_PANEL_DIR}/.custom-metrics-managed"
|
|
11
|
+
CONTROL_PANEL_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.control-panel.plist"
|
|
12
|
+
FILTER_PLIST="${LAUNCH_AGENTS_ROOT}/com.git-ai.skill-usage-filter.plist"
|
|
13
|
+
CONTROL_PANEL_LABEL="gui/$(id -u)/com.git-ai.control-panel"
|
|
14
|
+
FILTER_LABEL="gui/$(id -u)/com.git-ai.skill-usage-filter"
|
|
15
|
+
|
|
16
|
+
case "${GIT_AI_ROOT}" in
|
|
17
|
+
"${HOME}/.git-ai") ;;
|
|
18
|
+
*)
|
|
19
|
+
echo "拒绝卸载非默认目录:${GIT_AI_ROOT}" >&2
|
|
20
|
+
exit 1
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
23
|
+
|
|
24
|
+
launchctl bootout "${CONTROL_PANEL_LABEL}" >/dev/null 2>&1 || true
|
|
25
|
+
launchctl bootout "${FILTER_LABEL}" >/dev/null 2>&1 || true
|
|
26
|
+
|
|
27
|
+
if [ -f "${CUSTOM_METRICS_MARKER}" ]; then
|
|
28
|
+
/usr/bin/python3 - "${METRICS_PATH}" "${UPSTREAMS_PATH}" <<'PY'
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
path = Path(sys.argv[1])
|
|
36
|
+
upstreams_path = Path(sys.argv[2])
|
|
37
|
+
if not path.exists():
|
|
38
|
+
raise SystemExit(0)
|
|
39
|
+
if not upstreams_path.exists():
|
|
40
|
+
raise SystemExit("缺少原上报端点备份,已停止卸载以避免留下无效配置")
|
|
41
|
+
|
|
42
|
+
config = json.loads(path.read_text(encoding="utf-8"))
|
|
43
|
+
upstreams = json.loads(upstreams_path.read_text(encoding="utf-8"))
|
|
44
|
+
routes = upstreams.get("routes", upstreams)
|
|
45
|
+
route_keys = {
|
|
46
|
+
"/legacy/commit": "commit_endpoint",
|
|
47
|
+
"/legacy/checkpoint": "checkpoint_endpoint",
|
|
48
|
+
"/legacy/token-usage": "token_usage_endpoint",
|
|
49
|
+
"/prompt-duration": "prompt_duration_endpoint",
|
|
50
|
+
"/commit": "commit_endpoint_v2",
|
|
51
|
+
"/checkpoint": "checkpoint_endpoint_v2",
|
|
52
|
+
"/token-usage": "token_usage_endpoint_v2",
|
|
53
|
+
"/skill-usage": "skill_usage_endpoint_v2",
|
|
54
|
+
"/agent-usage": "agent_usage_endpoint_v2",
|
|
55
|
+
"/prompt-report": "prompt_report_endpoint_v2",
|
|
56
|
+
}
|
|
57
|
+
for route, key in route_keys.items():
|
|
58
|
+
url = routes.get(route)
|
|
59
|
+
if isinstance(url, str) and url:
|
|
60
|
+
config[key] = url
|
|
61
|
+
|
|
62
|
+
descriptor, temporary_name = tempfile.mkstemp(
|
|
63
|
+
prefix=f".{path.name}.",
|
|
64
|
+
suffix=".tmp",
|
|
65
|
+
dir=path.parent,
|
|
66
|
+
)
|
|
67
|
+
try:
|
|
68
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
69
|
+
json.dump(config, handle, ensure_ascii=False, indent=2)
|
|
70
|
+
handle.write("\n")
|
|
71
|
+
handle.flush()
|
|
72
|
+
os.fsync(handle.fileno())
|
|
73
|
+
os.chmod(temporary_name, 0o600)
|
|
74
|
+
os.replace(temporary_name, path)
|
|
75
|
+
except Exception:
|
|
76
|
+
try:
|
|
77
|
+
os.unlink(temporary_name)
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
pass
|
|
80
|
+
raise
|
|
81
|
+
PY
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
rm -f "${CONTROL_PANEL_PLIST}" "${FILTER_PLIST}" "${FILTER_SCRIPT}"
|
|
85
|
+
rm -rf "${CONTROL_PANEL_DIR}"
|
|
86
|
+
|
|
87
|
+
echo "卸载完成。保留了以下用户数据:"
|
|
88
|
+
echo " ${GIT_AI_ROOT}/config.json"
|
|
89
|
+
echo " ${GIT_AI_ROOT}/filter_plugins.json"
|
|
90
|
+
echo " ${GIT_AI_ROOT}/upstream_metrics.json"
|
|
91
|
+
echo " ${GIT_AI_ROOT}/filters/*.log"
|