trip-moments-skill 1.1.1 → 1.1.2
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/.agents/skills/trip-moments/scripts/clawbridge.py +16 -7
- package/.agents/skills/trip-moments/scripts/selftest.py +81 -28
- package/.agents/skills/trip-moments/scripts/studio.py +39 -3
- package/.agents/skills/trip-moments/scripts/studio_ui.py +20 -0
- package/bin/install.js +1 -1
- package/package.json +1 -1
|
@@ -591,13 +591,22 @@ def _studio_instruction_impl(action: str, ctx: dict, state: dict, base: str = ""
|
|
|
591
591
|
" (也可逐格调用 POST /api/framing;ratio 为 null 表示保留原图)\n"
|
|
592
592
|
"4. 只回几行:列出你把哪几格定成了什么画幅、保留了哪几格原图。")
|
|
593
593
|
|
|
594
|
-
if action == "caption":
|
|
595
|
-
return (head + "\n请做:为这组九宫格写朋友圈文案。\n"
|
|
596
|
-
+ _slot_lines(slots) + "\n\n"
|
|
597
|
-
"写 3 版不同语气(文艺诗意 / 纪实行记 / 松弛日常),每版 1-3 句、可直接发。\n"
|
|
598
|
-
"**只写画面里真实存在的东西**;括号里是选片时的标注,必要时核图确认。"
|
|
599
|
-
"如果哪几张的标注和画面不符,在最后用一行提醒我。\n"
|
|
600
|
-
"
|
|
594
|
+
if action == "caption":
|
|
595
|
+
return (head + "\n请做:为这组九宫格写朋友圈文案。\n"
|
|
596
|
+
+ _slot_lines(slots) + "\n\n"
|
|
597
|
+
"写 3 版不同语气(文艺诗意 / 纪实行记 / 松弛日常),每版 1-3 句、可直接发。\n"
|
|
598
|
+
"**只写画面里真实存在的东西**;括号里是选片时的标注,必要时核图确认。"
|
|
599
|
+
"如果哪几张的标注和画面不符,在最后用一行提醒我。\n"
|
|
600
|
+
"写好后**必须登记到工作台**:只说在会话里不算完成,用户在工作台文案面板上"
|
|
601
|
+
"会一直看到「还没有文案」,打包时也拿不到:\n"
|
|
602
|
+
f" POST {base}/api/caption\n"
|
|
603
|
+
' {"narrative": "一句话叙事线(可留空)",\n'
|
|
604
|
+
' "versions": [{"style": "文艺诗意", "text": "…"},\n'
|
|
605
|
+
' {"style": "纪实行记", "text": "…"},\n'
|
|
606
|
+
' {"style": "松弛日常", "text": "…"}],\n'
|
|
607
|
+
' "tags": ["可选标签"]}\n'
|
|
608
|
+
"(versions 至少一版;登记后工作台会立即显示并可逐版复制)\n"
|
|
609
|
+
"最后只回一两句:三版各是什么语气。不要复述全文,用户在工作台上就能看到。")
|
|
601
610
|
|
|
602
611
|
if action == "title":
|
|
603
612
|
return (head + f"\n请做:给第 {slot_no} 格拟标题。\n"
|
|
@@ -2930,24 +2930,29 @@ def t_claw_grade_candidates_and_output_visibility():
|
|
|
2930
2930
|
|
|
2931
2931
|
|
|
2932
2932
|
def t_agent_model_grade_candidates():
|
|
2933
|
-
"""Agent
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2933
|
+
"""Agent 自带模型轨道:每一步的结果都要落到工作台上,不能只在会话里说。
|
|
2934
|
+
|
|
2935
|
+
2026-10-04 用户实测两处空白:
|
|
2936
|
+
a) 挂着 Agent 会话点「生成调色方案」,页面既没有 克制/标准/浓郁 三行,也没有每行的
|
|
2937
|
+
预览/采用 按钮。三处协同缺口:简报把「工作台会自动生成三套候选」改写掉了、
|
|
2938
|
+
agent_turn 的 work() 不像 clawhive 轨道那样从落地的配方推导候选、前端又在
|
|
2939
|
+
useAgentModel() 下把候选面板整个隐藏。
|
|
2940
|
+
b) 点「生成 3 版文案」后,文案面板仍然回「还没有文案」——因为 Agent 根本没有能
|
|
2941
|
+
登记文案的接口(选片/裁切/调色/修图/出片都有,只有文案没有)。
|
|
2942
|
+
|
|
2943
|
+
这里用真实 HTTP 跑 /api/agent/send,会话的 run() 换成「通过工作台自己的接口落地结果」
|
|
2944
|
+
——真实 agent 就是这么做的——然后断言:三档候选真的随任务结果回到页面;Agent 只说不
|
|
2945
|
+
做时不拿旧配方冒充本轮方案;文案登记后 state 真的带得回来、空数组会被拒。
|
|
2943
2946
|
"""
|
|
2944
2947
|
import tempfile as _tf
|
|
2945
2948
|
import threading
|
|
2946
2949
|
import time as _time
|
|
2950
|
+
import urllib.error
|
|
2947
2951
|
import urllib.request
|
|
2948
2952
|
from pathlib import Path as _P
|
|
2949
2953
|
|
|
2950
2954
|
import agent_session
|
|
2955
|
+
import clawbridge
|
|
2951
2956
|
import studio
|
|
2952
2957
|
import studio_ui
|
|
2953
2958
|
|
|
@@ -2958,19 +2963,34 @@ def t_agent_model_grade_candidates():
|
|
|
2958
2963
|
old_started = getattr(studio.StudioHandler, "started", None)
|
|
2959
2964
|
box = {}
|
|
2960
2965
|
|
|
2966
|
+
def post(path, payload):
|
|
2967
|
+
req = urllib.request.Request(
|
|
2968
|
+
f"http://127.0.0.1:{box['port']}{path}",
|
|
2969
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
2970
|
+
headers={"Content-Type": "application/json"}, method="POST")
|
|
2971
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
2972
|
+
return r.status, json.loads(r.read())
|
|
2973
|
+
|
|
2961
2974
|
def fake_run(self, message, progress=None, cancelled=None):
|
|
2962
|
-
# A real agent on this track
|
|
2963
|
-
#
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2975
|
+
# A real agent on this track lands its result through the workbench's own
|
|
2976
|
+
# endpoint, so everything downstream must be derived from APPLIED state.
|
|
2977
|
+
mode = box.get("mode")
|
|
2978
|
+
text = "这一轮我没动工作台。"
|
|
2979
|
+
if mode == "grade":
|
|
2980
|
+
status, _ = post("/api/grade", {"slot": 5, "preset": "cine",
|
|
2981
|
+
"grade": {"exposure": 0.2, "highlights": -20,
|
|
2982
|
+
"shadows": 15, "dehaze": 12}})
|
|
2983
|
+
assert status == 200, f"agent 落地配方失败: HTTP {status}"
|
|
2984
|
+
text = "第 5 格我上了电影青橙。"
|
|
2985
|
+
elif mode == "caption":
|
|
2986
|
+
status, _ = post("/api/caption", {
|
|
2987
|
+
"narrative": "沿着海岸线走了一天",
|
|
2988
|
+
"versions": [{"style": "文艺诗意", "text": "潮声比人声多"},
|
|
2989
|
+
{"style": "纪实行记", "text": "今天走了两万步"},
|
|
2990
|
+
{"style": "松弛日常", "text": "风大,头发乱"}],
|
|
2991
|
+
"tags": ["海边"]})
|
|
2992
|
+
assert status == 200, f"agent 登记文案失败: HTTP {status}"
|
|
2993
|
+
text = "三版写好了,已登记到工作台。"
|
|
2974
2994
|
# Mirror how the real run() ends: the terminal event is what takes the
|
|
2975
2995
|
# session out of ACTIVE, so the next turn is allowed to start.
|
|
2976
2996
|
self.live_text = text
|
|
@@ -3007,12 +3027,12 @@ def t_agent_model_grade_candidates():
|
|
|
3007
3027
|
box["port"] = port
|
|
3008
3028
|
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
|
3009
3029
|
try:
|
|
3010
|
-
def send_turn():
|
|
3030
|
+
def send_turn(step="grade", operation="grade_plan"):
|
|
3011
3031
|
req = urllib.request.Request(
|
|
3012
3032
|
f"http://127.0.0.1:{port}/api/agent/send",
|
|
3013
|
-
data=json.dumps({"step":
|
|
3014
|
-
"operation":
|
|
3015
|
-
"message": "
|
|
3033
|
+
data=json.dumps({"step": step, "action": "work_step",
|
|
3034
|
+
"operation": operation, "slot": 5,
|
|
3035
|
+
"message": "请用你自己的模型处理这一步"}).encode("utf-8"),
|
|
3016
3036
|
headers={"Content-Type": "application/json"}, method="POST")
|
|
3017
3037
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
3018
3038
|
assert r.status == 202, f"Agent 回合没进任务队列: HTTP {r.status}"
|
|
@@ -3029,7 +3049,7 @@ def t_agent_model_grade_candidates():
|
|
|
3029
3049
|
raise AssertionError(f"agent_turn 没跑完: {jid}")
|
|
3030
3050
|
|
|
3031
3051
|
# ① agent 通过工作台接口落了配方 -> 必须带三档候选
|
|
3032
|
-
box["
|
|
3052
|
+
box["mode"] = "grade"
|
|
3033
3053
|
job = wait_job(send_turn())
|
|
3034
3054
|
assert job["status"] == "done", f"agent 回合失败: {job.get('error') or job}"
|
|
3035
3055
|
res = (job.get("result") or {}).get("result") or {}
|
|
@@ -3046,11 +3066,40 @@ def t_agent_model_grade_candidates():
|
|
|
3046
3066
|
assert (res.get("text") or ""), "agent 的话也要在同一个结果里"
|
|
3047
3067
|
|
|
3048
3068
|
# ② 一轮什么都不改的 agent -> 不得拿旧配方冒充本轮方案
|
|
3049
|
-
box["
|
|
3069
|
+
box["mode"] = "none"
|
|
3050
3070
|
job2 = wait_job(send_turn())
|
|
3051
3071
|
res2 = (job2.get("result") or {}).get("result") or {}
|
|
3052
3072
|
assert not (res2.get("candidates") or []), \
|
|
3053
3073
|
"agent 没动工作台却给了候选(旧配方被冒充成本轮方案)"
|
|
3074
|
+
|
|
3075
|
+
# ③ 文案:agent 登记后必须进 workspace(能刷新回来、算作本步已完成)
|
|
3076
|
+
box["mode"] = "caption"
|
|
3077
|
+
job3 = wait_job(send_turn(step="caption", operation="caption"))
|
|
3078
|
+
assert job3["status"] == "done", f"文案回合失败: {job3.get('error') or job3}"
|
|
3079
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/api/state", timeout=30) as r:
|
|
3080
|
+
state = json.loads(r.read())
|
|
3081
|
+
caps = (state.get("captions") or {}).get("versions") or []
|
|
3082
|
+
assert len(caps) == 3, f"agent 登记的文案没进 workspace: {state.get('captions')}"
|
|
3083
|
+
assert caps[0]["style"] == "文艺诗意" and caps[0]["text"], caps
|
|
3084
|
+
assert (state.get("captions") or {}).get("narrative") == "沿着海岸线走了一天"
|
|
3085
|
+
|
|
3086
|
+
# ④ 空文案必须被拒:不能把「没写」当成「已登记」
|
|
3087
|
+
try:
|
|
3088
|
+
post("/api/caption", {"versions": []})
|
|
3089
|
+
raise AssertionError("空 versions 不应被接受")
|
|
3090
|
+
except urllib.error.HTTPError as exc:
|
|
3091
|
+
assert exc.code == 400, f"空文案应回 400,实际 {exc.code}"
|
|
3092
|
+
try:
|
|
3093
|
+
post("/api/caption", {"versions": [" "]})
|
|
3094
|
+
raise AssertionError("全空白文案不应被接受")
|
|
3095
|
+
except urllib.error.HTTPError as exc:
|
|
3096
|
+
assert exc.code == 400, f"全空白文案应回 400,实际 {exc.code}"
|
|
3097
|
+
|
|
3098
|
+
# ⑤ 简报必须告诉 agent 登记哪个接口,否则它只会写在会话里
|
|
3099
|
+
brief = clawbridge.studio_instruction(
|
|
3100
|
+
"caption", {"slots": [1]}, state, f"http://127.0.0.1:{port}")
|
|
3101
|
+
assert "/api/caption" in brief, "文案简报没给出登记接口"
|
|
3102
|
+
assert "POST" in brief and "versions" in brief, "文案简报没有可照抄的请求体"
|
|
3054
3103
|
finally:
|
|
3055
3104
|
agent_session.AgentSession.run = real_run
|
|
3056
3105
|
httpd.shutdown()
|
|
@@ -3080,6 +3129,10 @@ def t_agent_model_grade_candidates():
|
|
|
3080
3129
|
"agent 只说不做时页面又保持沉默(用户分不清是 bug 还是没改动)"
|
|
3081
3130
|
assert "S.aiCandidates = null" in branch, \
|
|
3082
3131
|
"agent 没落地配方时没有清掉上一轮的候选(旧方案会被当成本轮产物)"
|
|
3132
|
+
assert "akind === \"caption\"" in branch, \
|
|
3133
|
+
"agent_turn 完成后没有处理文案(面板又会一直回「还没有文案」)"
|
|
3134
|
+
assert "没把文案登记到工作台" in branch, \
|
|
3135
|
+
"agent 只把文案写在会话里时页面不说明"
|
|
3083
3136
|
|
|
3084
3137
|
|
|
3085
3138
|
CASES = {
|
|
@@ -2146,7 +2146,8 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2146
2146
|
"/api/reset": self._api_reset,
|
|
2147
2147
|
"/api/undo": self._api_undo,
|
|
2148
2148
|
"/api/edit-source": self._api_edit_source,
|
|
2149
|
-
"/api/edit-result": self._api_edit_result,
|
|
2149
|
+
"/api/edit-result": self._api_edit_result,
|
|
2150
|
+
"/api/caption": self._api_caption,
|
|
2150
2151
|
"/api/edit-scan": self._api_edit_scan,
|
|
2151
2152
|
"/api/art": self._api_art,
|
|
2152
2153
|
"/api/art-preview": self._api_art_preview,
|
|
@@ -2172,8 +2173,8 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2172
2173
|
if fn is None:
|
|
2173
2174
|
self.send_json({"ok": False, "error": f"未知接口 {path}"}, 404)
|
|
2174
2175
|
return
|
|
2175
|
-
if path in {"/api/framing", "/api/grade", "/api/swap", "/api/replace",
|
|
2176
|
-
"/api/reset", "/api/undo", "/api/edit-source"}:
|
|
2176
|
+
if path in {"/api/framing", "/api/grade", "/api/swap", "/api/replace",
|
|
2177
|
+
"/api/reset", "/api/undo", "/api/edit-source", "/api/caption"}:
|
|
2177
2178
|
# Snapshot + mutation + render must be atomic even when an
|
|
2178
2179
|
# agent and a browser edit the same workspace concurrently.
|
|
2179
2180
|
with self.ws._lock:
|
|
@@ -2948,6 +2949,41 @@ class StudioHandler(BaseHTTPRequestHandler):
|
|
|
2948
2949
|
"summary": self.ws.grade_summary(slot),
|
|
2949
2950
|
"state": self.ws.state()["slots"][n - 1]})
|
|
2950
2951
|
|
|
2952
|
+
def _api_caption(self, body: dict) -> None:
|
|
2953
|
+
"""Persist caption versions produced outside the workbench (both agent tracks).
|
|
2954
|
+
|
|
2955
|
+
The local gateway track has always written captions into the workspace
|
|
2956
|
+
(see _api_ai), but an agent had no equivalent endpoint: it could only say
|
|
2957
|
+
the three versions in the conversation, so the 文案 step kept showing
|
|
2958
|
+
「还没有文案」 — on the very screen where the user had just asked for them —
|
|
2959
|
+
and the set never counted as finished. Registration stores exactly what
|
|
2960
|
+
the caller sends; nothing is invented here.
|
|
2961
|
+
"""
|
|
2962
|
+
raw = body.get("versions")
|
|
2963
|
+
if not isinstance(raw, list):
|
|
2964
|
+
self.send_json({"ok": False, "error": "versions 必须是数组"}, 400)
|
|
2965
|
+
return
|
|
2966
|
+
versions = []
|
|
2967
|
+
for item in raw:
|
|
2968
|
+
if isinstance(item, str):
|
|
2969
|
+
text, style = item.strip(), ""
|
|
2970
|
+
elif isinstance(item, dict):
|
|
2971
|
+
text = str(item.get("text") or "").strip()
|
|
2972
|
+
style = str(item.get("style") or "").strip()
|
|
2973
|
+
else:
|
|
2974
|
+
continue
|
|
2975
|
+
if text:
|
|
2976
|
+
versions.append({"style": style or f"版本 {len(versions) + 1}",
|
|
2977
|
+
"text": text[:4000]})
|
|
2978
|
+
if not versions:
|
|
2979
|
+
self.send_json({"ok": False, "error": "至少需要一版非空文案"}, 400)
|
|
2980
|
+
return
|
|
2981
|
+
tags = [str(t).strip() for t in (body.get("tags") or []) if str(t).strip()][:12]
|
|
2982
|
+
saved = self.ws.save_captions({"narrative": str(body.get("narrative") or "")[:600],
|
|
2983
|
+
"versions": versions, "tags": tags})
|
|
2984
|
+
self.send_json({"ok": True, "captions": saved, "versions": saved["versions"],
|
|
2985
|
+
"revision": self.ws.revision})
|
|
2986
|
+
|
|
2951
2987
|
def _api_edit_result(self, body: dict) -> None:
|
|
2952
2988
|
"""Register a retouch result produced outside the workbench.
|
|
2953
2989
|
|
|
@@ -2242,6 +2242,26 @@ async function handleJobDone(j) {
|
|
|
2242
2242
|
(<code>POST /api/grade</code>),或改用「模型网关」直接生成方案。</div>`;
|
|
2243
2243
|
}
|
|
2244
2244
|
toast("Agent 没改动工作台,所以还没有三档候选可选", "err", 7000);
|
|
2245
|
+
} else if (akind === "caption") {
|
|
2246
|
+
// 文案 is the one step with no adopt concept: it lands in the workspace and
|
|
2247
|
+
// the user copies a version. Before /api/caption existed an agent could
|
|
2248
|
+
// only say the three versions in the conversation, so this very panel kept
|
|
2249
|
+
// answering 「还没有文案」 right after the user asked for them.
|
|
2250
|
+
const caps = (S.data.captions && S.data.captions.versions) || [];
|
|
2251
|
+
if (caps.length) {
|
|
2252
|
+
renderCaptions();
|
|
2253
|
+
toast(`Agent 已登记 ${caps.length} 版文案,在文案面板逐版复制`, "ai", 6000);
|
|
2254
|
+
} else {
|
|
2255
|
+
const step = (j.meta || {}).step || jobStep(j);
|
|
2256
|
+
const box = step === S.step ? outBox() : null;
|
|
2257
|
+
if (box) {
|
|
2258
|
+
box.innerHTML = `<div class="note" style="margin-top:10px"><b>Agent 没把文案登记到工作台</b><br>
|
|
2259
|
+
三版文案可能只写在会话里了,所以本步仍是空的、打包也拿不到。
|
|
2260
|
+
回答在下方「本步回复与反馈」里;可让 Agent 重新调用 <code>POST /api/caption</code>
|
|
2261
|
+
登记,或改用「模型网关」生成。</div>`;
|
|
2262
|
+
}
|
|
2263
|
+
toast("Agent 没有登记文案,文案面板仍是空的", "err", 7000);
|
|
2264
|
+
}
|
|
2245
2265
|
}
|
|
2246
2266
|
return;
|
|
2247
2267
|
}
|
package/bin/install.js
CHANGED