codebee 0.1.99 → 0.1.100
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 +6 -0
- package/README.md +4 -5
- package/app/core/acp_client.py +426 -0
- package/app/core/backend_profiles.py +188 -0
- package/app/core/builtin_agent.py +12 -0
- package/app/core/command_auth.py +302 -0
- package/app/core/gitmod.py +6 -2
- package/app/core/jobs.py +3 -2
- package/app/core/manager.py +59 -4
- package/app/core/market_remote.py +20 -7
- package/app/core/openapi.py +32 -0
- package/app/core/pipeline.py +28 -5
- package/app/core/project_memory.py +73 -6
- package/app/core/registry.py +13 -0
- package/app/core/runner.py +13 -1
- package/app/main.py +70 -0
- package/app/ui/app.js +81 -1
- package/app/ui/index.html +19 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import stat
|
|
9
10
|
import time
|
|
10
11
|
from pathlib import Path
|
|
@@ -95,6 +96,8 @@ def list_entries(workdir, *, now=None):
|
|
|
95
96
|
raw = row.get("content") or ""
|
|
96
97
|
row["verified"] = hashlib.sha256(raw.encode("utf-8")).hexdigest() == row.get("content_sha256")
|
|
97
98
|
row["expired"] = float(row.get("expires_at") or 0) <= stamp
|
|
99
|
+
row["feedback_score"] = int(row.get("feedback_score") or 0)
|
|
100
|
+
row["feedback_count"] = int(row.get("feedback_count") or 0)
|
|
98
101
|
return sorted(rows, key=lambda x: (x.get("created_at", 0), x.get("id", "")), reverse=True)
|
|
99
102
|
|
|
100
103
|
|
|
@@ -126,14 +129,78 @@ def decide(workdir, entry_id, action, *, expected_version=None, now=None):
|
|
|
126
129
|
return row
|
|
127
130
|
|
|
128
131
|
|
|
129
|
-
def
|
|
132
|
+
def feedback(workdir, entry_id, *, useful, expected_version=None, now=None):
|
|
133
|
+
"""Record explicit retrieval feedback as a versioned append-only update."""
|
|
134
|
+
if not isinstance(useful, bool):
|
|
135
|
+
raise ValueError("useful 必须是布尔值")
|
|
136
|
+
path = _file(workdir)
|
|
137
|
+
row = _read(path).get(str(entry_id or ""))
|
|
138
|
+
if not row:
|
|
139
|
+
raise KeyError("记忆条目不存在")
|
|
140
|
+
if expected_version is not None and int(expected_version) != int(row.get("version") or 1):
|
|
141
|
+
raise ValueError("记忆条目版本已变化,请刷新后重试")
|
|
142
|
+
raw = row.get("content") or ""
|
|
143
|
+
if hashlib.sha256(raw.encode("utf-8")).hexdigest() != row.get("content_sha256"):
|
|
144
|
+
raise ValueError("记忆内容指纹校验失败")
|
|
145
|
+
if row.get("status") != "approved":
|
|
146
|
+
raise ValueError("只有已批准的记忆条目可以反馈")
|
|
130
147
|
stamp = float(now if now is not None else time.time())
|
|
148
|
+
row = dict(row, feedback_score=int(row.get("feedback_score") or 0) +
|
|
149
|
+
(1 if useful else -1),
|
|
150
|
+
feedback_count=int(row.get("feedback_count") or 0) + 1,
|
|
151
|
+
feedback_at=stamp, version=int(row.get("version") or 1) + 1,
|
|
152
|
+
updated_at=stamp)
|
|
153
|
+
_append(path, row)
|
|
154
|
+
return row
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _query_terms(query):
|
|
158
|
+
query = str(query or "").strip().lower()
|
|
159
|
+
words = re.findall(r"[a-z0-9_]+|[\u3400-\u9fff]+", query)
|
|
160
|
+
terms = set()
|
|
161
|
+
for word in words:
|
|
162
|
+
if len(word) > 2 and re.fullmatch(r"[\u3400-\u9fff]+", word):
|
|
163
|
+
terms.update(word[i:i + 2] for i in range(len(word) - 1))
|
|
164
|
+
else:
|
|
165
|
+
terms.add(word)
|
|
166
|
+
return terms
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _relevance(row, terms):
|
|
170
|
+
if not terms:
|
|
171
|
+
return 0
|
|
172
|
+
title = str(row.get("title") or "").lower()
|
|
173
|
+
content = str(row.get("content") or "").lower()
|
|
174
|
+
title_terms = _query_terms(title)
|
|
175
|
+
content_terms = _query_terms(content)
|
|
176
|
+
return 4 * len(terms & title_terms) + len(terms & content_terms)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def active_text(workdir, *, cap=4000, query="", now=None):
|
|
180
|
+
stamp = float(now if now is not None else time.time())
|
|
181
|
+
try:
|
|
182
|
+
cap = max(0, int(cap))
|
|
183
|
+
except (TypeError, ValueError):
|
|
184
|
+
cap = 4000
|
|
185
|
+
heading = "## 已批准项目记忆(内容已校验,过期条目不注入)\n\n"
|
|
186
|
+
cap = max(0, cap - len(heading) - 2)
|
|
187
|
+
terms = _query_terms(query)
|
|
131
188
|
items = []
|
|
132
189
|
for row in list_entries(workdir, now=stamp):
|
|
133
190
|
if row.get("status") != "approved" or row.get("expired") or not row.get("verified"):
|
|
134
191
|
continue
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
192
|
+
block = "### %s\n%s" % (row.get("title") or "项目记忆", row.get("content") or "")
|
|
193
|
+
items.append((row, block))
|
|
194
|
+
items.sort(key=lambda item: (_relevance(item[0], terms),
|
|
195
|
+
item[0].get("feedback_score", 0),
|
|
196
|
+
item[0].get("created_at", 0)), reverse=True)
|
|
197
|
+
selected = []
|
|
198
|
+
used = 0
|
|
199
|
+
for _row, block in items:
|
|
200
|
+
cost = len(block) + (2 if selected else 0)
|
|
201
|
+
if used + cost > cap:
|
|
202
|
+
continue
|
|
203
|
+
selected.append(block)
|
|
204
|
+
used += cost
|
|
205
|
+
text = "\n\n".join(selected)
|
|
206
|
+
return (heading + text + "\n\n") if text else ""
|
package/app/core/registry.py
CHANGED
|
@@ -88,6 +88,12 @@ def effective_agents(catalog_entries, detected):
|
|
|
88
88
|
if not pref.get("enabled", entry.get("default_enabled", False)):
|
|
89
89
|
continue
|
|
90
90
|
out.append(_build_agent(entry))
|
|
91
|
+
try:
|
|
92
|
+
from . import backend_profiles
|
|
93
|
+
out.extend(backend_profiles.runtime_agents())
|
|
94
|
+
except Exception:
|
|
95
|
+
# Broken local profile storage must not disable unrelated installed agents.
|
|
96
|
+
pass
|
|
91
97
|
out.extend([dict(m) for m in MOCK_AGENTS])
|
|
92
98
|
return out
|
|
93
99
|
|
|
@@ -98,6 +104,13 @@ def installed_agent(entry_id, catalog_entries, detected):
|
|
|
98
104
|
续会话是用户对该 CLI 的显式指定,不依赖其是否参与自动路由;
|
|
99
105
|
未安装 / 无编排配置 / id 不存在时返回 None。
|
|
100
106
|
"""
|
|
107
|
+
if str(entry_id or "").startswith("acp-"):
|
|
108
|
+
try:
|
|
109
|
+
from . import backend_profiles
|
|
110
|
+
return next((agent for agent in backend_profiles.runtime_agents()
|
|
111
|
+
if agent.get("id") == entry_id), None)
|
|
112
|
+
except Exception:
|
|
113
|
+
return None
|
|
101
114
|
for entry in catalog_entries:
|
|
102
115
|
if entry.get("id") != entry_id:
|
|
103
116
|
continue
|
package/app/core/runner.py
CHANGED
|
@@ -1678,7 +1678,8 @@ def _opencode_fast_banner_death(res):
|
|
|
1678
1678
|
|
|
1679
1679
|
def run_agent(agent, prompt, workdir=None, readonly=True,
|
|
1680
1680
|
timeout=DEFAULT_TIMEOUT, cancel_event=None, log_path=None, resume=None,
|
|
1681
|
-
images=None, require_tools=False, deadline=None
|
|
1681
|
+
images=None, require_tools=False, deadline=None, command_context=None,
|
|
1682
|
+
checkpoint_run_id=""):
|
|
1682
1683
|
"""执行一次智能体调用,返回统一结构
|
|
1683
1684
|
{ok, text, json, cost_usd, tokens, error, error_code, raw}。
|
|
1684
1685
|
agent 来自 registry.effective_agents();resume 为已有会话 id,仅真实智能体生效
|
|
@@ -1722,6 +1723,17 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
|
|
|
1722
1723
|
"kind": agent.get("kind", "generic"), "model": None,
|
|
1723
1724
|
"attempts": []}
|
|
1724
1725
|
kind = agent.get("kind", "generic")
|
|
1726
|
+
if kind == "acp":
|
|
1727
|
+
from . import acp_client
|
|
1728
|
+
context = dict(command_context or {})
|
|
1729
|
+
context.setdefault("run_id", checkpoint_run_id)
|
|
1730
|
+
context.setdefault("sandbox", {})
|
|
1731
|
+
context.setdefault("allow_terminal", not readonly)
|
|
1732
|
+
return acp_client.run_agent(
|
|
1733
|
+
agent, prompt, workdir=workdir, readonly=readonly, timeout=timeout,
|
|
1734
|
+
cancel_event=cancel_event, deadline=deadline,
|
|
1735
|
+
command_context=context, checkpoint_run_id=checkpoint_run_id,
|
|
1736
|
+
resume=resume)
|
|
1725
1737
|
if agent.get("runtime_config_sync_failed"):
|
|
1726
1738
|
return {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
|
|
1727
1739
|
"tokens": 0, "usage": None,
|
package/app/main.py
CHANGED
|
@@ -358,6 +358,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
358
358
|
return self._json(200, {"entries": project_memory.list_entries(task.get("workdir"))})
|
|
359
359
|
except (OSError, ValueError) as exc:
|
|
360
360
|
return self._json(409, {"error": str(exc)})
|
|
361
|
+
if path == "/api/backend-profiles":
|
|
362
|
+
from core import backend_profiles
|
|
363
|
+
try:
|
|
364
|
+
return self._json(200, {"profiles": backend_profiles.list_profiles()})
|
|
365
|
+
except (OSError, ValueError) as exc:
|
|
366
|
+
return self._json(409, {"error": str(exc)})
|
|
367
|
+
if path == "/api/command-auth":
|
|
368
|
+
from core import command_auth
|
|
369
|
+
return self._json(200, command_auth.view())
|
|
361
370
|
if path == "/api/agents/presence":
|
|
362
371
|
from core import presence
|
|
363
372
|
return self._json(200, {"agents": presence.list_agents()})
|
|
@@ -887,6 +896,58 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
887
896
|
return deny
|
|
888
897
|
if path == "/api/tasks":
|
|
889
898
|
return self._api_create_task()
|
|
899
|
+
if path == "/api/backend-profiles":
|
|
900
|
+
from core import backend_profiles
|
|
901
|
+
try:
|
|
902
|
+
return self._json(200, {"profile": backend_profiles.save(self._body() or {})})
|
|
903
|
+
except KeyError as exc:
|
|
904
|
+
return self._json(404, {"error": str(exc)})
|
|
905
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
906
|
+
return self._json(400, {"error": str(exc)})
|
|
907
|
+
m = re.match(r"^/api/backend-profiles/([^/]+)/delete$", path)
|
|
908
|
+
if m:
|
|
909
|
+
from core import backend_profiles
|
|
910
|
+
try:
|
|
911
|
+
backend_profiles.remove(unquote(m.group(1)))
|
|
912
|
+
store.bump_state()
|
|
913
|
+
return self._json(200, {"ok": True})
|
|
914
|
+
except KeyError as exc:
|
|
915
|
+
return self._json(404, {"error": str(exc)})
|
|
916
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
917
|
+
return self._json(400, {"error": str(exc)})
|
|
918
|
+
if path == "/api/command-auth":
|
|
919
|
+
from core import command_auth
|
|
920
|
+
body = self._body() or {}
|
|
921
|
+
try:
|
|
922
|
+
if "mode" in body:
|
|
923
|
+
command_auth.set_mode(body.get("mode"))
|
|
924
|
+
return self._json(200, command_auth.view())
|
|
925
|
+
except (TypeError, ValueError) as exc:
|
|
926
|
+
return self._json(400, {"error": str(exc)})
|
|
927
|
+
m = re.match(r"^/api/command-auth/requests/([^/]+)$", path)
|
|
928
|
+
if m:
|
|
929
|
+
from core import command_auth
|
|
930
|
+
body = self._body() or {}
|
|
931
|
+
try:
|
|
932
|
+
result = command_auth.decide(
|
|
933
|
+
unquote(m.group(1)), approve=body.get("approve"),
|
|
934
|
+
scope=body.get("scope", "once"), match=body.get("match", "exact"),
|
|
935
|
+
prefix=body.get("prefix"))
|
|
936
|
+
return self._json(200, result)
|
|
937
|
+
except KeyError as exc:
|
|
938
|
+
return self._json(404, {"error": str(exc)})
|
|
939
|
+
except (TypeError, ValueError) as exc:
|
|
940
|
+
return self._json(400, {"error": str(exc)})
|
|
941
|
+
m = re.match(r"^/api/command-auth/rules/([^/]+)/revoke$", path)
|
|
942
|
+
if m:
|
|
943
|
+
from core import command_auth
|
|
944
|
+
try:
|
|
945
|
+
command_auth.revoke(unquote(m.group(1)))
|
|
946
|
+
return self._json(200, {"ok": True})
|
|
947
|
+
except KeyError as exc:
|
|
948
|
+
return self._json(404, {"error": str(exc)})
|
|
949
|
+
except (TypeError, ValueError) as exc:
|
|
950
|
+
return self._json(400, {"error": str(exc)})
|
|
890
951
|
if path == "/api/a2a/tasks":
|
|
891
952
|
status, resp = self._create_and_start(self._body() or {})
|
|
892
953
|
return self._json(status, {"protocol": "a2a-lite", **resp})
|
|
@@ -915,6 +976,11 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
915
976
|
body = self._body() or {}
|
|
916
977
|
from core import project_memory
|
|
917
978
|
try:
|
|
979
|
+
if str(body.get("action") or "").lower() == "feedback":
|
|
980
|
+
row = project_memory.feedback(
|
|
981
|
+
task.get("workdir"), body.get("id"), useful=body.get("useful"),
|
|
982
|
+
expected_version=body.get("expected_version"))
|
|
983
|
+
return self._json(200, {"entry": row})
|
|
918
984
|
row = project_memory.decide(task.get("workdir"), body.get("id"),
|
|
919
985
|
body.get("action"),
|
|
920
986
|
expected_version=body.get("expected_version"))
|
|
@@ -3480,6 +3546,10 @@ def main():
|
|
|
3480
3546
|
print("[CodeBee] v%s" % (_ver or "?"), flush=True)
|
|
3481
3547
|
_step("正在准备数据目录…")
|
|
3482
3548
|
paths.ensure_dirs()
|
|
3549
|
+
from core import backend_profiles as _backend_profiles
|
|
3550
|
+
from core import command_auth as _command_auth
|
|
3551
|
+
_backend_profiles.init()
|
|
3552
|
+
_command_auth.init()
|
|
3483
3553
|
from core import attachments
|
|
3484
3554
|
n_pc = attachments.cleanup_stale() # 待提交附件残留清理(崩溃/弃单不堆积)
|
|
3485
3555
|
if n_pc:
|
package/app/ui/app.js
CHANGED
|
@@ -4764,7 +4764,8 @@ window.loadProjectMemory = async function (taskId) {
|
|
|
4764
4764
|
box.innerHTML = entries.map((entry) => {
|
|
4765
4765
|
const canApprove = entry.status === "pending" && !entry.expired && entry.verified;
|
|
4766
4766
|
const canRestore = entry.status === "archived" && !entry.expired && entry.verified;
|
|
4767
|
-
const
|
|
4767
|
+
const feedback = entry.status === "approved" ? '<button type="button" class="ghost small" onclick="feedbackProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',true,' + Number(entry.version) + ')">' + esc(t("有用")) + '</button><button type="button" class="ghost small" onclick="feedbackProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',false,' + Number(entry.version) + ')">' + esc(t("无用")) + '</button>' : "";
|
|
4768
|
+
const actions = feedback + (canApprove ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'approve\',' + Number(entry.version) + ')">' + esc(t("批准")) + "</button><button type=\"button\" class=\"ghost small\" onclick=\"decideProjectMemory('" + jsq(taskId) + "','" + jsq(entry.id) + "','reject'," + Number(entry.version) + ')">' + esc(t("拒绝")) + "</button>" : "") +
|
|
4768
4769
|
(entry.status === "approved" ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'archive\',' + Number(entry.version) + ')">' + esc(t("归档")) + "</button>" : "") +
|
|
4769
4770
|
(canRestore ? '<button type="button" class="ghost small" onclick="decideProjectMemory(\'' + jsq(taskId) + '\',\'' + jsq(entry.id) + '\',\'restore\',' + Number(entry.version) + ')">' + esc(t("恢复")) + "</button>" : "");
|
|
4770
4771
|
return '<article class="rd-memory-row"><div><strong>' + esc(entry.title || t("项目记忆")) + '</strong><span class="chip">' + esc(entry.status || "") + (entry.expired ? " · " + esc(t("已过期")) : "") + '</span></div><pre>' + esc(entry.content || "") + '</pre><div class="rd-memory-actions">' + actions + "</div></article>";
|
|
@@ -4782,6 +4783,16 @@ window.decideProjectMemory = async function (taskId, id, action, version) {
|
|
|
4782
4783
|
} catch (error) { toast(t("项目记忆操作失败:") + error.message, true); }
|
|
4783
4784
|
};
|
|
4784
4785
|
|
|
4786
|
+
window.feedbackProjectMemory = async function (taskId, id, useful, version) {
|
|
4787
|
+
try {
|
|
4788
|
+
await api("/api/tasks/" + encodeURIComponent(taskId) + "/memory", {
|
|
4789
|
+
method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()),
|
|
4790
|
+
body: JSON.stringify({ id, action: "feedback", useful, expected_version: version }),
|
|
4791
|
+
});
|
|
4792
|
+
await loadProjectMemory(taskId);
|
|
4793
|
+
} catch (error) { toast(t("项目记忆反馈失败:") + error.message, true); }
|
|
4794
|
+
};
|
|
4795
|
+
|
|
4785
4796
|
window.loadRunCheckpoints = async function (requestedRunId) {
|
|
4786
4797
|
const runId = requestedRunId || S.detailRunId || ((S.state || {}).task_latest || {})[S.detailTaskKey]?.id;
|
|
4787
4798
|
if (!runId) return;
|
|
@@ -13362,8 +13373,77 @@ async function loadSettings() {
|
|
|
13362
13373
|
}
|
|
13363
13374
|
} catch (e) { /* 忽略 */ }
|
|
13364
13375
|
loadSettingsV2(); // 引擎调参卡独立拉取(挂了不影响基础设置)
|
|
13376
|
+
loadBackendProfiles();
|
|
13377
|
+
loadCommandAuth();
|
|
13365
13378
|
}
|
|
13366
13379
|
|
|
13380
|
+
async function loadBackendProfiles() {
|
|
13381
|
+
const box = $("acp-profiles");
|
|
13382
|
+
if (!box) return;
|
|
13383
|
+
try {
|
|
13384
|
+
const data = await api("/api/backend-profiles");
|
|
13385
|
+
const rows = data.profiles || [];
|
|
13386
|
+
box.innerHTML = rows.length ? rows.map((row) =>
|
|
13387
|
+
'<div class="list-row"><span><b>' + esc(row.label || row.id) + '</b> · ' + esc(row.command || "") +
|
|
13388
|
+
(row.enabled === false ? ' · ' + esc(t("已停用")) : '') + '</span><button class="ghost small" type="button" onclick="deleteAcpProfile(\'' + jsq(row.id) + '\')">' + esc(t("删除")) + '</button></div>'
|
|
13389
|
+
).join("") : '<span class="hint">' + esc(t("暂无 ACP 后端")) + '</span>';
|
|
13390
|
+
} catch (error) {
|
|
13391
|
+
box.innerHTML = '<span class="hint">' + esc(t("ACP 配置加载失败:")) + esc(error.message) + '</span>';
|
|
13392
|
+
}
|
|
13393
|
+
}
|
|
13394
|
+
|
|
13395
|
+
window.saveAcpProfile = async function () {
|
|
13396
|
+
const msg = $("acp-msg");
|
|
13397
|
+
try {
|
|
13398
|
+
const argsText = ($("acp-args") || {}).value || "[]";
|
|
13399
|
+
const args = JSON.parse(argsText);
|
|
13400
|
+
if (!Array.isArray(args)) throw new Error(t("参数必须是 JSON 数组"));
|
|
13401
|
+
await api("/api/backend-profiles", { method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()),
|
|
13402
|
+
body: JSON.stringify({ label: $("acp-label").value, command: $("acp-command").value,
|
|
13403
|
+
args, workspace_path: $("acp-workspace").value }) });
|
|
13404
|
+
if (msg) msg.textContent = t("ACP 后端已保存");
|
|
13405
|
+
["acp-label", "acp-command", "acp-args", "acp-workspace"].forEach((id) => { const el = $(id); if (el) el.value = id === "acp-args" ? "[]" : ""; });
|
|
13406
|
+
await loadBackendProfiles();
|
|
13407
|
+
await refreshState();
|
|
13408
|
+
} catch (error) { if (msg) msg.textContent = t("ACP 保存失败:") + error.message; }
|
|
13409
|
+
};
|
|
13410
|
+
|
|
13411
|
+
window.deleteAcpProfile = async function (id) {
|
|
13412
|
+
try {
|
|
13413
|
+
await api("/api/backend-profiles/" + encodeURIComponent(id) + "/delete", { method: "POST", headers: authHeaders(), body: "{}" });
|
|
13414
|
+
await loadBackendProfiles();
|
|
13415
|
+
await refreshState();
|
|
13416
|
+
} catch (error) { toast(t("删除 ACP 失败:") + error.message, true); }
|
|
13417
|
+
};
|
|
13418
|
+
|
|
13419
|
+
async function loadCommandAuth() {
|
|
13420
|
+
const pendingBox = $("command-auth-pending");
|
|
13421
|
+
const mode = $("command-auth-mode");
|
|
13422
|
+
if (!pendingBox || !mode) return;
|
|
13423
|
+
try {
|
|
13424
|
+
const data = await api("/api/command-auth");
|
|
13425
|
+
mode.value = data.mode || "legacy";
|
|
13426
|
+
const rows = data.pending || [];
|
|
13427
|
+
pendingBox.innerHTML = rows.length ? rows.map((row) =>
|
|
13428
|
+
'<div class="list-row"><span><code>' + esc(row.command || "") + '</code> · ' + esc(row.source || "") + '</span><button class="ghost small" type="button" onclick="decideCommandAuth(\'' + jsq(row.id) + '\',true)">' + esc(t("批准一次")) + '</button><button class="ghost small" type="button" onclick="decideCommandAuth(\'' + jsq(row.id) + '\',false)">' + esc(t("拒绝")) + '</button></div>'
|
|
13429
|
+
).join("") : '<span class="hint">' + esc(t("暂无待批准命令")) + '</span>';
|
|
13430
|
+
} catch (error) { pendingBox.innerHTML = '<span class="hint">' + esc(t("授权状态加载失败:")) + esc(error.message) + '</span>'; }
|
|
13431
|
+
}
|
|
13432
|
+
|
|
13433
|
+
window.saveCommandAuthMode = async function () {
|
|
13434
|
+
try {
|
|
13435
|
+
await api("/api/command-auth", { method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()), body: JSON.stringify({ mode: $("command-auth-mode").value }) });
|
|
13436
|
+
await loadCommandAuth();
|
|
13437
|
+
} catch (error) { const msg = $("command-auth-msg"); if (msg) msg.textContent = t("授权策略保存失败:") + error.message; }
|
|
13438
|
+
};
|
|
13439
|
+
|
|
13440
|
+
window.decideCommandAuth = async function (id, approve) {
|
|
13441
|
+
try {
|
|
13442
|
+
await api("/api/command-auth/requests/" + encodeURIComponent(id), { method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders()), body: JSON.stringify({ approve, scope: "once" }) });
|
|
13443
|
+
await loadCommandAuth();
|
|
13444
|
+
} catch (error) { const msg = $("command-auth-msg"); if (msg) msg.textContent = t("命令授权操作失败:") + error.message; }
|
|
13445
|
+
};
|
|
13446
|
+
|
|
13367
13447
|
/* ---------------- settings_v2 引擎调参卡(schema 驱动) ----------------
|
|
13368
13448
|
* /api/settings-v2 拉 describe(含字段元数据),按 namespace 渲染控件;
|
|
13369
13449
|
* 保存走 /api/settings-v2/<ns> mutate(expected_revision CAS,409=别处已改,
|
package/app/ui/index.html
CHANGED
|
@@ -1076,6 +1076,25 @@
|
|
|
1076
1076
|
</div>
|
|
1077
1077
|
<p data-i18n="内置智能体(直连对话/直接执行)会把服务器的工具并入工具循环(mcp__服务器__工具 名)。命令在你本机执行,与「执行命令」工具同一自主权级别;工具清单缓存 5 分钟。留空 = 不启用。" class="hint">内置智能体(直连对话/直接执行)会把服务器的工具并入工具循环(mcp__服务器__工具 名)。命令在你本机执行,与「执行命令」工具同一自主权级别;工具清单缓存 5 分钟。留空 = 不启用。</p>
|
|
1078
1078
|
</div>
|
|
1079
|
+
<h3 class="sec-title">ACP Backend Profiles</h3>
|
|
1080
|
+
<p class="hint">接入遵循 Agent Client Protocol v1 的本地或 SSH stdio Agent。密钥只保存在本机,读取时始终脱敏。</p>
|
|
1081
|
+
<div class="field">
|
|
1082
|
+
<div id="acp-profiles" class="list"></div>
|
|
1083
|
+
<div class="input-row" style="margin-top:6px">
|
|
1084
|
+
<input id="acp-label" type="text" placeholder="名称">
|
|
1085
|
+
<input id="acp-command" type="text" placeholder="启动命令,例如 acp-agent">
|
|
1086
|
+
<input id="acp-args" type="text" placeholder="参数 JSON 数组,例如 [\"--stdio\"]">
|
|
1087
|
+
<input id="acp-workspace" type="text" placeholder="Agent 工作区(可空)">
|
|
1088
|
+
<button class="ghost small" type="button" onclick="saveAcpProfile()">保存 ACP</button>
|
|
1089
|
+
</div>
|
|
1090
|
+
<p id="acp-msg" class="hint"></p>
|
|
1091
|
+
</div>
|
|
1092
|
+
<h3 class="sec-title">命令授权</h3>
|
|
1093
|
+
<div class="field">
|
|
1094
|
+
<div class="input-row"><select id="command-auth-mode"><option value="legacy">legacy:自动执行</option><option value="restricted">restricted:逐条等待批准</option></select><button class="ghost small" type="button" onclick="saveCommandAuthMode()">保存策略</button></div>
|
|
1095
|
+
<div id="command-auth-pending" class="list"></div>
|
|
1096
|
+
<p id="command-auth-msg" class="hint"></p>
|
|
1097
|
+
</div>
|
|
1079
1098
|
<h3 data-i18n="推送通知" class="sec-title">推送通知</h3>
|
|
1080
1099
|
<div class="field"><label data-i18n="群机器人 Webhook(钉钉 / 飞书 / 企业微信,按域名自动适配)">群机器人 Webhook(钉钉 / 飞书 / 企业微信,按域名自动适配)</label>
|
|
1081
1100
|
<div class="input-row">
|
package/package.json
CHANGED