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.
@@ -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 自带模型轨道也必须给出三档候选 + 预览/采用入口(2026-10-04 用户实测)。
2934
-
2935
- 用户挂着 Agent 会话点「生成调色方案」,页面既没有 克制/标准/浓郁 三行,也没有每行
2936
- 的 预览/采用 按钮。缺口是三处协同造成的:发给 agent 的简报把那句「工作台会自动生成
2937
- 三套候选」改写掉了、agent_turn 的 work() 不像 clawhive 轨道(studio.py 里 grade
2938
- 分支)那样从落地的配方推导候选、前端又在 useAgentModel() 下把候选面板整个隐藏。
2939
-
2940
- 这里用真实 HTTP 跑 /api/agent/send:会话的 run() 换成「通过工作台自己的 /api/grade
2941
- 把配方落地」——真实 agent 就是这么做的——然后断言三档候选真的随任务结果回到页面;
2942
- 再跑一轮什么都不改的 agent,断言不会拿旧配方冒充本轮的方案。
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 applies its recipe through the workbench's
2963
- # own endpoint, so the candidates must be derived from APPLIED state.
2964
- text = "第 5 格我上了电影青橙。"
2965
- if box.get("apply"):
2966
- req = urllib.request.Request(
2967
- f"http://127.0.0.1:{box['port']}/api/grade",
2968
- data=json.dumps({"slot": 5, "preset": "cine",
2969
- "grade": {"exposure": 0.2, "highlights": -20,
2970
- "shadows": 15, "dehaze": 12}}).encode("utf-8"),
2971
- headers={"Content-Type": "application/json"}, method="POST")
2972
- with urllib.request.urlopen(req, timeout=30) as r:
2973
- assert r.status == 200, f"agent 落地配方失败: HTTP {r.status}"
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": "grade", "action": "work_step",
3014
- "operation": "grade_plan", "slot": 5,
3015
- "message": "请用你自己的模型给我调色"}).encode("utf-8"),
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["apply"] = True
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["apply"] = False
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
@@ -16,7 +16,7 @@ const path = require('path');
16
16
  const os = require('os');
17
17
  const { execSync, spawnSync } = require('child_process');
18
18
 
19
- const VERSION = '1.1.1';
19
+ const VERSION = '1.1.2';
20
20
  const SKILL_NAME = 'trip-moments';
21
21
 
22
22
  const c = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trip-moments-skill",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "AI 朋友圈旅行出图助手 (trip-moments skill) — 工业级全流程旅行摄影智能出图与朋友圈九宫格套件",
5
5
  "main": "bin/install.js",
6
6
  "bin": {