trip-moments-skill 1.1.2 → 1.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.
@@ -28,18 +28,51 @@ AGENT_STYLES = r"""
28
28
  .ap-compose textarea{width:100%;min-height:65px;max-height:160px;resize:vertical;padding:9px;border:1px solid var(--line-2);border-radius:7px;font-size:12px}
29
29
  .ap-compose .row{margin-top:8px}.ap-compose .btn{white-space:normal}
30
30
  .ap-next{width:100%;margin-top:8px}
31
- .workflow-control{padding:13px;border:1px solid var(--line-2);border-radius:9px;background:var(--card);margin-bottom:13px}
32
- .workflow-control h3{font-size:12px;margin-bottom:8px}
33
- .workflow-mode{display:grid;grid-template-columns:1fr 1fr;gap:5px}
31
+ .workflow-mode{display:grid;grid-template-columns:1fr 1fr;gap:5px;min-width:0}
34
32
  .workflow-mode button{padding:8px 5px;border:1px solid var(--line-2);border-radius:6px;background:var(--paper-hi);cursor:pointer;font-size:11px}
35
33
  .workflow-mode button.on{background:var(--navy);border-color:var(--navy);color:#fff}
36
- .workflow-control .row{margin-top:9px}.workflow-control .row .btn{flex:1}
37
- .workflow-control .note{margin-top:8px}
38
- .agent-workflow{border:1px solid #cbd7c8;border-radius:9px;background:var(--card);margin:12px 0;overflow:hidden}
39
- .agent-workflow>header{padding:10px 12px;border-bottom:1px solid var(--line);font-size:12px;font-weight:600}
40
- .agent-workflow .ap-stream{padding:10px;max-height:360px;overflow:auto}
41
- .agent-workflow .ap-status{margin:0 0 8px}
42
- .agent-workflow .ap-compose{padding:10px}
34
+ /* ---- the per-step action deck -------------------------------------------
35
+ 写要求 → 点按钮 → 看结果 in one card. Before this the request box sat in the
36
+ middle card, the trigger at the top of the rail and the result at the bottom,
37
+ and five labels told the user to click 「上方」. */
38
+ .deck{border:1px solid var(--line-2);border-radius:10px;background:var(--card);margin-bottom:13px;overflow:hidden}
39
+ .deck-h{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line);
40
+ background:linear-gradient(120deg,#f7f5ee,#eef2ee)}
41
+ .deck-t{font-size:12px;font-weight:600}.deck-s{font-size:10px;color:var(--sub);text-align:right}
42
+ .deck-ctl{padding:10px 12px 2px}
43
+ .deck-row{display:flex;align-items:center;gap:9px;margin-bottom:8px}
44
+ .deck-k{font-size:10.5px;color:var(--sub);flex:none;width:52px}
45
+ .deck-row>.workflow-mode{flex:1}
46
+ .deck-row select{flex:1;min-width:0;padding:7px;border:1px solid var(--line-2);border-radius:6px;background:#fff;font-size:11px}
47
+ .deck-ctl #wf-mode-note{margin:0 0 10px}
48
+ .deck-write{padding:0 12px}
49
+ .deck-write .pref-grid{margin-top:2px}
50
+ .deck-write .cp-actions{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px}
51
+ .deck-write .cp-actions button{font-size:11px;padding:6px 10px;border:1px solid #becdbb;border-radius:6px;
52
+ background:#f4f8f2;color:#2c4a37;cursor:pointer;transition:all .16s var(--ease)}
53
+ .deck-write .cp-actions button:hover{background:#fff;transform:translateY(-1px)}
54
+ .deck-l{display:block;font-size:10.5px;color:var(--sub);margin:2px 0 5px}
55
+ .deck-write textarea{width:100%;min-height:62px;max-height:170px;resize:vertical;padding:9px;font-size:12px;
56
+ border:1px solid var(--line-2);border-radius:7px;background:#fff;font-family:inherit}
57
+ .deck-go{display:flex;gap:7px;flex-wrap:wrap;padding:10px 12px 0}
58
+ .deck-go .btn{white-space:normal}
59
+ .deck-go #wf-work{flex:1;min-width:120px}
60
+ .deck-go .note{margin:0}
61
+ #wf-connection{margin:8px 12px 0}
62
+ #wf-connection:empty{display:none}
63
+ .deck-brief{margin:9px 12px 0;border:1px solid var(--line);border-radius:7px;background:var(--paper-hi)}
64
+ .deck-brief>summary{font-size:10.5px;color:var(--sub);padding:8px 10px;cursor:pointer}
65
+ .deck-brief pre{white-space:pre-wrap;overflow-wrap:anywhere;font:10.5px/1.65 var(--mono);color:var(--ink-2);
66
+ max-height:300px;overflow:auto;padding:0 10px 9px;margin:0}
67
+ .deck-out{padding:11px 12px 12px;border-top:1px solid var(--line);margin-top:11px}
68
+ .deck-out-h{display:flex;align-items:center;gap:8px;font-size:10px;color:var(--sub);
69
+ text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px}
70
+ .deck-out-h .sp{flex:1}.deck-out-s{font-size:10px;color:var(--sub);text-transform:none;letter-spacing:0}
71
+ .deck-out .ai-out:empty{margin:0}
72
+ .deck-agent{border-top:1px dashed var(--line-2);padding:11px 12px 12px;background:#fbfcfa}
73
+ .deck-agent .ap-stream{max-height:340px;overflow:auto}
74
+ .deck-agent .ap-status{margin:0 0 8px}
75
+ .deck .proposal-card{margin:0 0 9px}
43
76
  @media(max-width:720px){.agent-panel{top:8px;bottom:8px;right:8px;width:calc(100vw - 16px)}}
44
77
  """
45
78
 
@@ -73,57 +106,70 @@ AGENT_SHELL = r"""
73
106
  AGENT_SCRIPT = r"""
74
107
  const AP = { open: false, data: null, polling: false, timer: null, seq: -1, pending: false, draft: "", answers: {} };
75
108
  function useAgentModel() { return S.data?.preferences?.ai_mode === "agent"; }
76
- function workflowControlsHtml() {
109
+ /* The deck's control rows. No trigger button here any more: the deck owns the
110
+ 写要求 → 点按钮 → 看结果 order, so the button sits under the input box. */
111
+ function deckControlsHtml() {
77
112
  const agent = useAgentModel();
78
- const labels = { select:"智能选片", crop:"生成构图方案", grade:"生成调色方案",
79
- retouch:"开始修图", finish:"生成所选作品", caption:"生成 3 版文案" };
80
- return `<section class="workflow-control"><h3>本步由谁处理</h3>
81
- <div class="workflow-mode" role="group" aria-label="AI 执行来源">
82
- <button class="${agent ? "" : "on"}" aria-pressed="${!agent}" data-ai-mode="gateway">模型网关</button>
83
- <button class="${agent ? "on" : ""}" aria-pressed="${agent}" data-ai-mode="agent">Agent 自带模型</button>
84
- </div>
85
- <p class="note" id="wf-mode-note">${agent ? "使用已绑定 Agent 的模型、工具与会话上下文。" : "使用配置的模型网关;AI 分析会发送照片预览,不进入 Agent 会话。"}</p>
86
- ${S.step === "finish" ? `<label class="note" for="finish-mode">本次制作</label>
87
- <select id="finish-mode" style="width:100%;padding:7px;border:1px solid var(--line-2);border-radius:6px">
88
- <option value="art" ${S.finishMode !== "local" ? "selected" : ""}>AI 艺术作品 · 使用所选模型来源</option>
89
- <option value="local" ${S.finishMode === "local" ? "selected" : ""}>本地画板/九宫格 · 不调用模型</option>
90
- </select><p class="note">在下方选择风格或版式,再点击「生成所选作品」。此步不打包。</p>` : ""}
91
- ${["crop","grade"].includes(S.step) ? `<label class="note" for="wf-scope">处理范围</label>
92
- <select id="wf-scope" style="width:100%;padding:7px;border:1px solid var(--line-2);border-radius:6px">
93
- <option value="current" ${S.aiScope !== "all" ? "selected" : ""}>当前第 ${S.sel} 格</option>
113
+ const sel = S.sel != null ? S.sel : 1;
114
+ return `<div class="deck-ctl">
115
+ <div class="deck-row"><span class="deck-k">谁来做</span>
116
+ <div class="workflow-mode" role="group" aria-label="AI 执行来源">
117
+ <button class="${agent ? "" : "on"}" aria-pressed="${!agent}" data-ai-mode="gateway">模型网关</button>
118
+ <button class="${agent ? "on" : ""}" aria-pressed="${agent}" data-ai-mode="agent">Agent 自带模型</button>
119
+ </div></div>
120
+ ${["crop","grade"].includes(S.step) ? `<div class="deck-row"><span class="deck-k">处理范围</span>
121
+ <select id="wf-scope" aria-label="处理范围">
122
+ <option value="current" ${S.aiScope !== "all" ? "selected" : ""}>当前第 ${sel} 格</option>
94
123
  <option value="all" ${S.aiScope === "all" ? "selected" : ""}>全部已选照片</option>
95
- </select>` : ""}
96
- <div class="row"><button class="btn btn-sm" id="wf-review" onclick="runCurrentStep('review')">检查本步</button>
97
- <button class="btn btn-sm btn-ai" id="wf-work" onclick="runCurrentStep('work')">${labels[S.step] || "处理本步"}</button></div>
98
- <p class="note" id="wf-connection" ${agent ? "" : 'style="display:none"'}>${agent ? "连接状态正在同步…" : ""}</p>
99
- ${agent ? `<button class="btn btn-sm ghost" onclick="openAgentPanel()">管理 Agent 连接</button>` : ""}
100
- </section>`;
124
+ </select></div>` : ""}
125
+ ${S.step === "finish" ? `<div class="deck-row"><span class="deck-k">本次制作</span>
126
+ <select id="finish-mode" aria-label="本次制作方式">
127
+ <option value="art" ${S.finishMode !== "local" ? "selected" : ""}>AI 艺术作品 · 用所选模型来源</option>
128
+ <option value="local" ${S.finishMode === "local" ? "selected" : ""}>本地画板/九宫格 · 不调模型</option>
129
+ </select></div>` : ""}
130
+ <p class="note" id="wf-mode-note">${agent
131
+ ? "用已绑定 Agent 的模型、工具与会话上下文;检查、处理、追问都在本卡完成。"
132
+ : "用配置的模型网关;AI 分析会发送照片预览,不进入 Agent 会话。"}</p>
133
+ </div>`;
134
+ }
135
+ /* One input box serves both 「本步要求」 and 「继续追问」, so the session buttons
136
+ live next to the trigger instead of under a second textarea. */
137
+ function deckSessionButtons() {
138
+ const active = !!AP.pending || !!(AP.data && AP.data.active);
139
+ if (active) {
140
+ return `<button class="btn btn-sm" id="ap-steer" onclick="controlAgent('steer')">补充到当前执行</button>
141
+ <button class="btn btn-sm ghost" id="ap-stop" onclick="controlAgent('stop')">停止本轮</button>`;
142
+ }
143
+ return `<button class="btn btn-sm" id="ap-send" onclick="sendAgentReply()">继续对话</button>`;
101
144
  }
102
- function agentWorkflowHtml() {
145
+ function agentStreamHtml() {
103
146
  if (!useAgentModel()) return "";
104
- return `<section class="agent-workflow" aria-label="当前步骤的 Agent 协作">
105
- <header>本步回复与反馈 · 同一会话</header>
147
+ return `<div class="deck-agent">
148
+ <div class="deck-out-h"><span>Agent 会话</span><span class="sp"></span>
149
+ <span class="deck-out-s">同一会话 · 回复不会自动跳步</span></div>
106
150
  <div class="ap-stream" id="ap-stream">
107
151
  <div class="ap-status" id="ap-run-status">正在读取绑定会话…</div>
108
152
  <div id="ap-messages"></div><div id="ap-live" class="ap-live" style="display:none" role="status"></div>
109
153
  <div id="ap-approval"></div>
110
154
  <details class="ap-tools"><summary>本轮真实工具事件</summary><div id="ap-events"></div></details>
111
155
  </div>
112
- <div class="ap-compose">
113
- <textarea id="ap-input" placeholder="继续追问、补充要求,或确认采用刚才的方案。仍然进入同一个 Agent 会话。" aria-label="给绑定 Agent 的回复">${H(AP.draft)}</textarea>
114
- <div class="row"><button class="btn btn-sm btn-primary" id="ap-send" onclick="sendAgentReply()">继续对话</button>
115
- <button class="btn btn-sm" id="ap-steer" onclick="controlAgent('steer')">补充到当前执行</button>
116
- <button class="btn btn-sm ghost" id="ap-stop" onclick="controlAgent('stop')">停止本轮</button></div>
117
- <button class="btn btn-sm ghost" style="margin-top:6px" onclick="syncAgentHistory()">同步会话回复</button>
118
- <p class="note" style="margin-top:6px">收到回复不会自动跳步。确认完成后,再使用本步底部的「进入下一步」。</p>
119
- </div></section>`;
156
+ <div class="row" style="margin-top:7px">
157
+ <button class="btn btn-sm ghost" onclick="syncAgentHistory()">同步会话回复</button>
158
+ <button class="btn btn-sm ghost" onclick="openAgentPanel()">管理 Agent 连接</button>
159
+ </div>
160
+ </div>`;
161
+ }
162
+ function deckInput() { return $("deck-intent"); }
163
+ function deckInputText() {
164
+ const el = deckInput();
165
+ return el ? el.value.trim() : (S.copilotIntent || "").trim();
120
166
  }
121
167
  function startAgentPolling() {
122
168
  if (!AP.timer) AP.timer = setInterval(() => {
123
169
  if (AP.open || useAgentModel() || AP.data?.active) pollAgent();
124
170
  }, 850);
125
171
  }
126
- function wireWorkflow() {
172
+ function wireDeckControls() {
127
173
  const finishMode = $("finish-mode");
128
174
  if (finishMode) finishMode.addEventListener("change", () => {
129
175
  S.finishMode = finishMode.value;
@@ -132,8 +178,6 @@ function wireWorkflow() {
132
178
  document.querySelectorAll("[data-ai-mode]").forEach(el => el.addEventListener("click", () => setAIMode(el.dataset.aiMode)));
133
179
  const scope = $("wf-scope");
134
180
  if (scope) scope.addEventListener("change", () => { S.aiScope = scope.value; });
135
- const input = $("ap-input");
136
- if (input) input.addEventListener("input", () => { AP.draft = input.value; });
137
181
  if (useAgentModel()) {
138
182
  startAgentPolling();
139
183
  AP.seq = -1; AP.approvalId = null;
@@ -187,7 +231,7 @@ async function runCurrentStep(kind) {
187
231
  if (useAgentModel()) {
188
232
  if (kind === "review") {
189
233
  if (!await requireAgentConnection()) return;
190
- return sendAgentMessage("review_step", $("cp-intent")?.value || "", S.step);
234
+ return sendAgentMessage("review_step", deckInputText(), S.step);
191
235
  }
192
236
  if (S.step === "retouch") return aiEdit();
193
237
  const operation = { select:"select9", crop:"compose", grade:S.aiScope === "all" ? "grade_all" : "grade_plan", retouch:"edit", finish:"finish", caption:"caption" }[S.step];
@@ -355,25 +399,31 @@ async function sendAgentMessage(action, message, step = S.step, options = {}) {
355
399
  try {
356
400
  const data = await post("/api/agent/send", { ...options, action, message, step, slot: S.sel });
357
401
  if (!data.ok) { toast(data.error, "err", 6500); return false; }
358
- AP.draft = ""; if ($("ap-input")) $("ap-input").value = ""; pollJobs(); pollAgent();
402
+ AP.draft = "";
403
+ const el = deckInput();
404
+ if (el) el.value = "";
405
+ S.copilotIntent = "";
406
+ (S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = "";
407
+ S.deckBrief = "";
408
+ pollJobs(); pollAgent();
359
409
  return true;
360
410
  } finally { AP.pending = false; }
361
411
  }
362
412
  function sendAgentStep(action) {
363
- return sendAgentMessage(action, ($("ap-input")?.value || AP.draft).trim());
413
+ return sendAgentMessage(action, deckInputText());
364
414
  }
365
415
  function sendAgentReply() {
366
- const text = ($("ap-input")?.value || AP.draft).trim();
416
+ const text = deckInputText();
367
417
  if (!text) { toast("请写下本轮回复或修改要求"); return; }
368
418
  return sendAgentMessage("", text);
369
419
  }
370
420
  async function controlAgent(action, answers) {
371
- const text = ($("ap-input")?.value || AP.draft).trim();
421
+ const text = deckInputText();
372
422
  if (action === "steer" && !text) { toast("请先写下要补充的要求"); return; }
373
423
  const data = await post("/api/agent/control", { action, text, answers,
374
424
  run_id: AP.data?.run_id, request_id: AP.data?.approval?.request_id });
375
425
  if (!data.ok) { toast(data.error, "err"); return; }
376
- if (action === "steer") { AP.draft = ""; if ($("ap-input")) $("ap-input").value = ""; }
426
+ if (action === "steer") { AP.draft = ""; const el = deckInput(); if (el) el.value = ""; }
377
427
  pollAgent();
378
428
  }
379
429
  function answerAgent() {
@@ -366,33 +366,26 @@ async function renderJourneyMap() {
366
366
  }
367
367
  }
368
368
 
369
- function copilotHtml() {
369
+ /* Quick direction chips for the deck. The input they used to own is now THE
370
+ deck input (#deck-intent) — one box per step, right above the trigger button,
371
+ so the old note telling the user to click the button 「上方」 is gone. */
372
+ function deckChipsHtml() {
370
373
  const s = curSlot();
371
- // Retouch already has a complete preset + instruction + submit workflow.
372
- // A second quick-action card duplicated the input and submitted prematurely.
373
374
  if (!s?.present || !["crop", "grade"].includes(S.step)) return "";
374
375
  const buttons = S.step === "crop"
375
376
  ? [["compose","保护主体","保留主体完整与原有空间感,不要机械套用竖裁"],["compose","更有张力","寻找更有叙事张力的画幅,保留重要建筑与人物"]]
376
377
  : S.step === "grade"
377
378
  ? [["grade_plan","自然通透","自然通透,克制饱和度,保持真实肤色"],["grade_plan","电影暖调","电影感暖色调,但不要压死暗部,肤色保持自然"],["grade_plan","清冷氛围","清冷克制的旅行画册影调,不要过度偏青"]]
378
379
  : [];
379
- return `<section class="copilot"><h3>AI 要求(可选)</h3>
380
- <p>选方向只填写要求,不会发起任务;确认后点击上方「生成${S.step === "crop" ? "构图" : "调色"}方案」。</p>
381
- <div class="cp-actions">${buttons.map(([action,label,intent]) =>
382
- `<button data-cp-action="${action}" data-cp-intent="${H(intent)}">${label}</button>`).join("")}</div>
383
- <input id="cp-intent" aria-label="本步 AI 要求" placeholder="可选:选择或填写本步要求" value="${H(S.copilotIntent || "")}" maxlength="800">
384
- </section>`;
380
+ return `<div class="cp-actions">${buttons.map(([action,label,intent]) =>
381
+ `<button data-cp-action="${action}" data-cp-intent="${H(intent)}">${label}</button>`).join("")}</div>`;
385
382
  }
386
383
 
387
384
  function wireCopilot() {
388
- const input = $("cp-intent");
389
- if (input) input.addEventListener("input", () => {
390
- S.copilotIntent = input.value;
391
- (S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = input.value;
392
- });
393
385
  document.querySelectorAll("[data-cp-action]").forEach(el => el.addEventListener("click", () => {
394
386
  S.copilotIntent = el.dataset.cpIntent;
395
387
  (S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = S.copilotIntent;
388
+ const input = $("deck-intent");
396
389
  if (input) input.value = S.copilotIntent;
397
390
  document.querySelectorAll("[data-cp-action]").forEach(button =>
398
391
  button.setAttribute("aria-pressed", String(button === el)));
@@ -2917,10 +2917,35 @@ def t_claw_grade_candidates_and_output_visibility():
2917
2917
  html = studio_ui.STUDIO_HTML
2918
2918
  assert 'id="ai-out"' not in html, \
2919
2919
  "还有面板在用重复的 id=\"ai-out\"(getElementById 只会拿到第一个,结果会画进隐藏面板)"
2920
- assert html.count('class="ai-out"') >= 6, \
2921
- "六个 step 应各有一个输出区,实际 %d 个" % html.count('class="ai-out"')
2922
- assert '[data-pane="${step}"] .ai-out' in html, \
2923
- "输出区没有按当前 step 解析"
2920
+ assert 'id="ai-out-${H(step)}"' in html, \
2921
+ "操作台没有给每一步自己的输出区 id(结果可能画到别的步)"
2922
+ assert '[data-pane="${step}"] .ai-out' in html, "输出区没有按当前 step 解析"
2923
+ assert html.count("actionDeckHtml(") >= 7, \
2924
+ "六个 step 应各接一个操作台(含函数定义共 7 处),实际 %d 处" % html.count("actionDeckHtml(")
2925
+ assert html.count('pane("') >= 6, "六个 step 面板数量不对"
2926
+ assert "getElementById(`ai-out-${step}`)" in html, \
2927
+ "outBox() 没有优先按 step 精确取输出区"
2928
+
2929
+ # 操作台的存在意义:写要求 → 点按钮 → 看结果 在同一张卡里,顺序不能倒。
2930
+ # 旧布局把触发按钮放在 rail 顶部、输入框放在中间卡片、结果放在最底部,
2931
+ # 于是五处正文直接叫用户「点击上方…」——用户抱怨“上下跑”。这里把那个
2932
+ # 设计约束固化成断言,而不是只写进注释。
2933
+ deck = html[html.index("function actionDeckHtml("):]
2934
+ deck = deck[:deck.index("async function loadDeckBrief")]
2935
+ assert deck.index('id="deck-intent"') < deck.index('id="wf-work"'), \
2936
+ "操作台里触发按钮又跑到输入框上面了(写→发 的顺序被倒过来)"
2937
+ assert deck.index('id="wf-work"') < deck.index('class="ai-out"'), \
2938
+ "操作台里结果区不在触发按钮下面(点完还要再找结果)"
2939
+ for bad in ("点击上方", "上方选择", "上方唯一的", "本页顶部"):
2940
+ assert bad not in html, f"还在叫用户往上看:「{bad}」——操作台就是为了去掉这种跳转"
2941
+ # 一个输入框:旧布局有「本步要求」和「继续追问」两个框,用户得猜往哪个里写。
2942
+ for gone in ('id="cp-intent"', 'id="ap-input"', 'id="inst"'):
2943
+ assert gone not in html, f"旧输入框 {gone} 又回来了(应只剩 #deck-intent 一个)"
2944
+ assert "function deckInputText" in html and "deckInputText()" in html, \
2945
+ "发送路径没有统一读操作台的输入框"
2946
+ assert html.count('id="deck-intent"') == 1, \
2947
+ "#deck-intent 应只在操作台模板里出现一次,实际 %d 次" % html.count('id="deck-intent"')
2948
+
2924
2949
  assert "p.export_zip || p.export_dir" not in html, \
2925
2950
  "导出目录的 Windows 路径又进了 href(浏览器里点了没反应)"
2926
2951
  assert "/api/reveal" in html, "出品台没有「在资源管理器里打开」的入口"
@@ -1646,7 +1646,7 @@ class Workspace:
1646
1646
  "slots": slots,
1647
1647
  "pool": pool,
1648
1648
  "pool_meta": {"total": len(pool) + sum(1 for s in slots if s["present"])},
1649
- "captions": self.captions,
1649
+ "captions": self.captions,
1650
1650
  "gps": _load_json(self.outdir / "gps.json", []),
1651
1651
  "products": products,
1652
1652
  "selection": getattr(self, "selection_extra", {}),
@@ -2228,6 +2228,13 @@ class StudioHandler(BaseHTTPRequestHandler):
2228
2228
  self.send_json(link.sync_history())
2229
2229
  elif path == "/api/agent/disconnect":
2230
2230
  self.send_json(link.disconnect())
2231
+ elif path == "/api/agent/brief":
2232
+ # Lazy: the brief can be several KB (the art one embeds a compiled
2233
+ # prompt card), so it is fetched when the deck's details is opened
2234
+ # rather than riding along on every /api/state poll.
2235
+ self.send_json({"ok": True,
2236
+ "brief": str(self.ws.extra.get("agent_brief") or ""),
2237
+ "at": str(self.ws.extra.get("agent_brief_at") or "")})
2231
2238
  elif path == "/api/agent/control":
2232
2239
  self.send_json(link.control(str(body.get("action") or ""), body))
2233
2240
  elif path == "/api/agent/send":
@@ -2317,6 +2324,12 @@ class StudioHandler(BaseHTTPRequestHandler):
2317
2324
  f"修图状态:{'已采用文件缺失' if s.get('edit_missing') else '新修图待采用' if s['pending_edit'] else '已采用修图' if s['edit_adopted'] else '原片处理版'}"
2318
2325
  for s in snapshot["slots"] if s["present"])
2319
2326
  outbound = context + "\n当前作品:\n" + summary + "\n\n用户本轮输入:\n" + message
2327
+ # Keep the brief the deck shows the user. Transparency is the point:
2328
+ # 「它有没有照做」 becomes checkable without reading the conversation.
2329
+ with self.ws._lock:
2330
+ self.ws.extra["agent_brief"] = context
2331
+ self.ws.extra["agent_brief_at"] = _now()
2332
+ self.ws.save()
2320
2333
  link.reserve(message)
2321
2334
  def work(job):
2322
2335
  result = link.run(outbound, lambda text, pct: (setattr(job, "detail", text), setattr(job, "progress", pct)),
@@ -706,7 +706,7 @@ kbd{font-family:var(--mono);font-size:9.5px;border:1px solid var(--line-2);borde
706
706
  <button class="btn btn-ai" onclick="saveCustomModel()">注册并应用</button>
707
707
  <div class="field" style="margin-top:16px"><label for="cfg-amap">高德 Web 服务 Key(路线卡,可选)</label>
708
708
  <input id="cfg-amap" type="password" autocomplete="off" placeholder="留空保留已有配置">
709
- <p class="note">仅高德路线卡需要;旅程页的在线地图不使用此 Key。填写后点击上方「保存设置」。</p></div>
709
+ <p class="note">仅高德路线卡需要;旅程页的在线地图不使用此 Key。填好后记得保存设置。</p></div>
710
710
  <div class="field" style="margin-top:24px;border-top:1px solid var(--line);padding-top:15px">
711
711
  <label style="display:flex;align-items:center;gap:8px;letter-spacing:0">
712
712
  <input id="cfg-show-claw" type="checkbox" style="width:auto" onchange="setClawVisibility(this.checked)">
@@ -1071,7 +1071,7 @@ function publishHeroHtml() {
1071
1071
  ${p.export_zip ? `<button class="btn btn-primary" onclick="saveZip()" ${stale ? "disabled" : ""}>下载全部 · zip</button>` : ""}
1072
1072
  <button class="btn btn-sm ghost" onclick="revealExport()">在资源管理器里打开</button>
1073
1073
  </div>
1074
- ${stale ? `<p class="note warn" style="margin-top:8px">这是上次导出,尚未确认与当前照片及参数一致。请先点击上方「打包发布」,不会把旧包当作最新成品下载。</p>` : ""}
1074
+ ${stale ? `<p class="note warn" style="margin-top:8px">这是上次导出,尚未确认与当前照片及参数一致。请先重新点「打包发布」,不会把旧包当作最新成品下载。</p>` : ""}
1075
1075
  <div class="note" style="margin-top:8px;font-family:var(--mono);font-size:9.5px"
1076
1076
  title="${H(p.export_dir || "")}">${H(shortPath(p.export_dir))}</div>
1077
1077
  </div>`;
@@ -1553,6 +1553,105 @@ $("canvas").addEventListener("click", (e) => {
1553
1553
  });
1554
1554
 
1555
1555
  /* ---------------- inspector ---------------- */
1556
+ /* ---------------- the per-step action deck ----------------
1557
+ 写要求 → 点按钮 → 看结果 used to be three separate places: the request box sat in
1558
+ the middle card, the trigger button at the top of the rail, and the result at the
1559
+ very bottom — five labels in the UI literally told the user to click the button
1560
+ 「上方」 because the layout
1561
+ forced a scroll up to act and back down to read. Everything a step needs to run
1562
+ now sits in one card in that order, and the manual controls (presets, sliders,
1563
+ ratios, art styles) are demoted below it. */
1564
+ const DECK_LABEL = { select: "智能选片", crop: "生成构图方案", grade: "生成调色方案",
1565
+ retouch: "开始修图", caption: "生成 3 版文案", finish: "生成所选作品" };
1566
+ const DECK_INTENT = {
1567
+ select: ["策展偏好补充", "可选:例如 主角不超过两张,多留一张夜景"],
1568
+ crop: ["构图要求", "可选:例如 保留主体完整与原有空间感,不要机械套用竖裁"],
1569
+ grade: ["调色要求", "可选:例如 电影感暖色调,但不要压死暗部,肤色保持自然"],
1570
+ retouch: ["修图指令", "例如:去掉画面右侧的游客"],
1571
+ caption: ["文案要求", "可选:例如 三版都别用感叹号,第二版交代地点"],
1572
+ finish: ["出片要求", "可选:例如 标题短一点,不要盖住人脸"],
1573
+ };
1574
+ function deckIntentLabel() { return (DECK_INTENT[S.step] || ["本步要求"])[0]; }
1575
+ function deckIntentPlaceholder() { return (DECK_INTENT[S.step] || ["本步要求", ""]).slice(-1)[0]; }
1576
+ function deckIntentValue() {
1577
+ // ONE box serves both 「本步要求」 and the Agent follow-up: it is the same
1578
+ // message to the same session, and keeping two boxes is what made the flow
1579
+ // feel like guessing which one to type in.
1580
+ return S.step === "retouch" ? (S.editInstruction || S.copilotIntent || "")
1581
+ : (S.copilotIntent || "");
1582
+ }
1583
+ function deckBriefHtml(promptBox) {
1584
+ if (!useAgentModel()) return promptBox || "";
1585
+ return `<details class="deck-brief" id="deck-brief" ${S.briefOpen ? "open" : ""}>
1586
+ <summary>本轮发给 Agent 的简报</summary>
1587
+ <pre id="deck-brief-body">${H(S.deckBrief || "还没有发过任务;发起后这里显示它实际收到的要求。")}</pre>
1588
+ </details>`;
1589
+ }
1590
+ function actionDeckHtml(o = {}) {
1591
+ const step = S.step;
1592
+ const agent = useAgentModel();
1593
+ const busy = agent ? !!(AP.pending || (AP.data && AP.data.active))
1594
+ : !!(S.aiBusy || S.aiSubmissions);
1595
+ return `<section class="deck" id="action-deck" data-deck-step="${H(step)}">
1596
+ <header class="deck-h"><span class="deck-t">本步操作台</span><span class="sp"></span>
1597
+ <span class="deck-s">${agent ? "写要求 → 点按钮 → 结果与会话都在下面"
1598
+ : "写要求 → 点按钮 → 结果就在下面"}</span></header>
1599
+ ${deckControlsHtml()}
1600
+ <div class="deck-write">
1601
+ ${o.chips || ""}
1602
+ <label class="deck-l" for="deck-intent">${H(deckIntentLabel())}</label>
1603
+ <textarea id="deck-intent" spellcheck="false" maxlength="800"
1604
+ placeholder="${H(deckIntentPlaceholder())}">${H(deckIntentValue())}</textarea>
1605
+ </div>
1606
+ <div class="deck-go">
1607
+ <button class="btn btn-sm" id="wf-review" onclick="runCurrentStep('review')">检查本步</button>
1608
+ <button class="btn btn-ai" id="wf-work" onclick="runCurrentStep('work')"${busy ? " disabled" : ""}>${H(DECK_LABEL[step] || "处理本步")}</button>
1609
+ ${agent ? deckSessionButtons() : ""}
1610
+ </div>
1611
+ <p class="note" id="wf-connection"${agent ? "" : ' style="display:none"'}></p>
1612
+ ${deckBriefHtml(o.prompt)}
1613
+ <div class="deck-out">
1614
+ <div class="deck-out-h"><span>结果</span><span class="sp"></span></div>
1615
+ ${o.result || ""}
1616
+ <div class="ai-out" id="ai-out-${H(step)}"></div>
1617
+ </div>
1618
+ ${agent ? agentStreamHtml() : ""}
1619
+ </section>`;
1620
+ }
1621
+
1622
+ async function loadDeckBrief() {
1623
+ const body = $("deck-brief-body");
1624
+ if (!body) return;
1625
+ const d = await post("/api/agent/brief", {});
1626
+ if (!d.ok) { body.textContent = d.error || "读不到简报"; return; }
1627
+ S.deckBrief = d.brief || "";
1628
+ body.textContent = S.deckBrief
1629
+ ? (d.at ? "发信时间 " + d.at + "\n\n" : "") + S.deckBrief
1630
+ : "还没有发过任务;发起后这里显示它实际收到的要求。";
1631
+ }
1632
+ function wireDeck() {
1633
+ const input = $("deck-intent");
1634
+ if (input) input.addEventListener("input", () => {
1635
+ // One store for all steps: S.copilotIntent is what aiRun(), delegateAIAction()
1636
+ // and promptCtx() already read, so the box the user types in is the box the
1637
+ // request is built from — no second copy to drift out of sync.
1638
+ S.copilotIntent = input.value;
1639
+ (S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = input.value;
1640
+ if (S.step === "retouch") { S.editInstruction = input.value; rememberEditDraft(); }
1641
+ AP.draft = input.value;
1642
+ });
1643
+ const brief = $("deck-brief");
1644
+ if (brief) {
1645
+ // Always re-read on open: the last brief changes every turn, and a stale one
1646
+ // would make 「它有没有照做」 unanswerable.
1647
+ brief.addEventListener("toggle", () => {
1648
+ S.briefOpen = brief.open;
1649
+ if (brief.open) loadDeckBrief();
1650
+ });
1651
+ if (brief.open) loadDeckBrief();
1652
+ }
1653
+ }
1654
+
1556
1655
  function renderInspector(passive = true) {
1557
1656
  if (!S.data) return;
1558
1657
  if (passive && (isEditing() || S.mutations)) { S.inspectorDeferred = true; return; }
@@ -1561,14 +1660,18 @@ function renderInspector(passive = true) {
1561
1660
  const scroll = box.scrollTop;
1562
1661
  const s = curSlot();
1563
1662
  if (!s) { box.innerHTML = `<div class="note" style="padding:40px 6px">没有可检查的格子。</div>`; return; }
1564
- if (!s.present && !["select", "finish", "caption"].includes(S.step)) {
1565
- box.innerHTML = `${workflowControlsHtml()}<div class="card"><div class="card-h"><span class="t">第 ${s.slot} 格是空的</span></div>
1566
- <div class="note">从照片库点一张照片即可填入这一格。</div>
1567
- <button class="btn" style="margin-top:12px" onclick="gotoStep('select',false)">去选片</button></div>
1568
- <div data-pane="${S.step}" class="on"><div class="ai-out"></div></div>${agentWorkflowHtml()}`;
1569
- wireWorkflow();
1570
- if (!useAgentModel() && S.outputs[S.step] && !S.outputSlots[S.step]) renderAiOut(S.outputs[S.step], S.step, null);
1571
- return;
1663
+ if (!s.present && !["select", "finish", "caption"].includes(S.step)) {
1664
+ box.innerHTML = `<div data-pane="${S.step}" class="on">
1665
+ ${actionDeckHtml()}
1666
+ <div class="card"><div class="card-h"><span class="t">第 ${s.slot} 格是空的</span></div>
1667
+ <div class="note">从照片库点一张照片即可填入这一格。</div>
1668
+ <button class="btn" style="margin-top:12px" onclick="gotoStep('select',false)">去选片</button></div>
1669
+ </div>`;
1670
+ wireInspector();
1671
+ wireDeck();
1672
+ wireDeckControls();
1673
+ if (S.outputs[S.step] && !S.outputSlots[S.step]) renderAiOut(S.outputs[S.step], S.step, null);
1674
+ return;
1572
1675
  }
1573
1676
  const g = S.data.grade, meta = g.meta;
1574
1677
  const dirty = (k) => Math.abs((S.params[k] || 0) - meta[k].default) > 1e-9;
@@ -1659,41 +1762,33 @@ function renderInspector(passive = true) {
1659
1762
  </button>`;
1660
1763
  }).join("");
1661
1764
 
1662
- const picked = new Set((S.data.slots || []).filter((x) => x.present).map((x) => x.source));
1663
- const selectPane = `
1664
- <div class="card">
1665
- <div class="card-h"><span class="t">策展风格问卷</span><span class="sp"></span>
1666
- <span class="s">高审美偏好引导</span></div>
1667
- <div class="note">
1668
- 选片决定全篇气质。选择心仪的策展风格(已启用人物去重与僵硬摆拍扣分算法):
1669
- </div>
1670
- <div class="pref-grid">${prefChips}</div>
1671
- <div class="note">选择偏好后,点击上方「智能选片」。手动换图不需要调用 AI。</div>
1672
- </div>
1673
-
1674
- <div class="card">
1675
- <div class="card-h"><span class="t">选片状态</span><span class="sp"></span>
1676
- <span class="s">${picked.size} / 9 已就位</span></div>
1677
- <div class="note" style="margin-bottom:9px">
1678
- 照片统一在左侧九宫格与候选池操作,不在这里重复展示。原始文件不会被修改。
1679
- </div>
1680
- <button class="btn btn-sm" onclick="focusPhotoLibrary()">前往照片库</button>
1681
- </div>
1682
-
1683
- <div class="card">
1684
- <div class="card-h"><span class="t">摆放顺序与节奏</span></div>
1685
- <div class="note">左侧九宫格支持直接拖动换位。
1686
- 黄金位法则:1号位抓眼开篇、5号位C位核心主角、9号位余韵收尾。</div>
1687
- <div class="row" style="margin-top:9px">
1688
- <button class="btn btn-sm" onclick="storyOrder('time')">按时间预演</button>
1689
- <button class="btn btn-sm" onclick="storyOrder('place')">按地点预演</button>
1690
- </div><div id="story-preview"></div>
1691
- <button class="btn btn-sm btn-ai" style="width:100%;margin-top:9px" onclick="aiRun('pick')">
1765
+ const picked = new Set((S.data.slots || []).filter((x) => x.present).map((x) => x.source));
1766
+ const selectPane = `
1767
+ ${actionDeckHtml({ chips: `
1768
+ <div class="note" style="margin-bottom:7px">选片决定全篇气质。已启用人物去重与僵硬摆拍扣分算法;手动换图不需要调用 AI。</div>
1769
+ <div class="pref-grid">${prefChips}</div>`, prompt: promptBox })}
1770
+
1771
+ <div class="card">
1772
+ <div class="card-h"><span class="t">选片状态</span><span class="sp"></span>
1773
+ <span class="s">${picked.size} / 9 已就位</span></div>
1774
+ <div class="note" style="margin-bottom:9px">
1775
+ 照片统一在左侧九宫格与候选池操作,不在这里重复展示。原始文件不会被修改。
1776
+ </div>
1777
+ <button class="btn btn-sm" onclick="focusPhotoLibrary()">前往照片库</button>
1778
+ </div>
1779
+
1780
+ <div class="card">
1781
+ <div class="card-h"><span class="t">摆放顺序与节奏</span></div>
1782
+ <div class="note">左侧九宫格支持直接拖动换位。
1783
+ 黄金位法则:1号位抓眼开篇、5号位C位核心主角、9号位余韵收尾。</div>
1784
+ <div class="row" style="margin-top:9px">
1785
+ <button class="btn btn-sm" onclick="storyOrder('time')">按时间预演</button>
1786
+ <button class="btn btn-sm" onclick="storyOrder('place')">按地点预演</button>
1787
+ </div><div id="story-preview"></div>
1788
+ <button class="btn btn-sm btn-ai" style="width:100%;margin-top:9px" onclick="aiRun('pick')">
1692
1789
  仅为当前已选照片建议排序
1693
- </button>
1694
- </div>
1695
- ${promptBox}
1696
- <div class="ai-out"></div>`;
1790
+ </button>
1791
+ </div>`;
1697
1792
 
1698
1793
  const stepMeta = STEP_BY_ID[S.step] || STEPS[0];
1699
1794
  const stepIdx = STEPS.findIndex((x) => x.id === stepMeta.id);
@@ -1729,123 +1824,102 @@ function renderInspector(passive = true) {
1729
1824
  <div class="note">确认照片与文案后再打包;未完成的步骤可以随时返回调整。</div>
1730
1825
  </div>`;
1731
1826
 
1732
- box.innerHTML = `
1733
- ${stepHead}
1734
- ${workflowControlsHtml()}
1827
+ box.innerHTML = `
1828
+ ${stepHead}
1735
1829
  <div class="step-panes">
1736
1830
  ${pane("select", selectPane)}
1737
- ${pane("crop", `
1738
- <div class="card" id="insp-frame">
1739
- <div class="card-h"><span class="t">画幅与裁切重心</span><span class="sp"></span>
1740
- <span class="s">${s.ratio ? s.ratio[0] + ":" + s.ratio[1] : "原图"}</span></div>
1741
- ${copilotHtml()}
1742
- <p class="note" style="margin-bottom:8px">手动构图:点击下面的比例或重心即生效,可在画布工具栏撤回。</p>
1743
- <div class="ratio-grid">${ratioCards}</div>
1744
- <div class="note" style="margin-top:11px">
1745
- <b>裁切重心</b>:把照片裁成上面选的画幅时,保留画面里的哪一块。
1746
- 点下面的小图,或点「点选裁切重心」后在画布上点一下主体位置。
1747
- </div>
1748
- <div class="aim-grid" id="aim-grid">
1749
- <i class="v" style="left:33.333%"></i><i class="v" style="left:66.666%"></i>
1750
- <i class="h" style="top:33.333%"></i><i class="h" style="top:66.666%"></i>
1751
- <span class="aim-dot" id="aim-dot" style="left:${s.anchor[0] * 100}%;top:${s.anchor[1] * 100}%"></span>
1752
- </div>
1753
- </div>
1754
- <div class="card" id="insp-info">
1755
- <div class="slot-head">
1756
- <span class="no">${String(s.slot).padStart(2, "0")}</span>
1757
- <div><div class="lb">${H(s.label || "精选位")}</div>
1758
- <div class="note" style="font-size:10px">${H(s.source)}</div></div>
1759
- <div class="spacer" style="flex:1"></div>
1760
- </div>
1761
- <dl class="kv">
1762
- ${s.scene ? `<dt>场景</dt><dd>${H(s.scene)}</dd>` : ""}
1763
- ${s.note ? `<dt>点评</dt><dd>${H(s.note)}</dd>` : ""}
1764
- ${s.score != null ? `<dt>选片分</dt><dd>${(s.score * 10).toFixed(1)} / 10</dd>` : ""}
1765
- ${s.color ? `<dt>主调</dt><dd><span class="swatch" style="background:${H(s.color)}"></span>${H(s.color)}</dd>` : ""}
1766
- </dl>
1767
- ${(s.reasons || []).length ? `<div class="chips" style="margin-top:9px">
1768
- ${s.reasons.map((r) => `<span class="chip">${H(r)}</span>`).join("")}</div>` : ""}
1769
- ${s.ai && s.ai.scene ? `<div class="note" style="margin-top:9px">AI 判断:${H(s.ai.scene)}</div>` : ""}
1770
- </div>
1771
- ${promptBox}
1772
- <div class="ai-out"></div>`)}
1773
- ${pane("grade", `
1774
- <div class="card" id="insp-grade">
1775
- <div class="card-h"><span class="t">手动胶片预设与影调</span><span class="sp"></span>
1776
- <button class="btn btn-sm ghost" onclick="resetGrade()">归零</button></div>
1831
+ ${pane("crop", `
1832
+ ${actionDeckHtml({ chips: deckChipsHtml(), result: candHtml, prompt: promptBox })}
1833
+ <div class="card" id="insp-frame">
1834
+ <div class="card-h"><span class="t">手动画幅与裁切重心</span><span class="sp"></span>
1835
+ <span class="s">${s.ratio ? s.ratio[0] + ":" + s.ratio[1] : "原图"}</span></div>
1836
+ <p class="note" style="margin-bottom:8px">点下面的比例或重心即生效,不需要点操作台的按钮,可在画布工具栏撤回。</p>
1837
+ <div class="ratio-grid">${ratioCards}</div>
1838
+ <div class="note" style="margin-top:11px">
1839
+ <b>裁切重心</b>:把照片裁成上面选的画幅时,保留画面里的哪一块。
1840
+ 点下面的小图,或点「点选裁切重心」后在画布上点一下主体位置。
1841
+ </div>
1842
+ <div class="aim-grid" id="aim-grid">
1843
+ <i class="v" style="left:33.333%"></i><i class="v" style="left:66.666%"></i>
1844
+ <i class="h" style="top:33.333%"></i><i class="h" style="top:66.666%"></i>
1845
+ <span class="aim-dot" id="aim-dot" style="left:${s.anchor[0] * 100}%;top:${s.anchor[1] * 100}%"></span>
1846
+ </div>
1847
+ </div>
1848
+ <div class="card" id="insp-info">
1849
+ <div class="slot-head">
1850
+ <span class="no">${String(s.slot).padStart(2, "0")}</span>
1851
+ <div><div class="lb">${H(s.label || "精选位")}</div>
1852
+ <div class="note" style="font-size:10px">${H(s.source)}</div></div>
1853
+ <div class="spacer" style="flex:1"></div>
1854
+ </div>
1855
+ <dl class="kv">
1856
+ ${s.scene ? `<dt>场景</dt><dd>${H(s.scene)}</dd>` : ""}
1857
+ ${s.note ? `<dt>点评</dt><dd>${H(s.note)}</dd>` : ""}
1858
+ ${s.score != null ? `<dt>选片分</dt><dd>${(s.score * 10).toFixed(1)} / 10</dd>` : ""}
1859
+ ${s.color ? `<dt>主调</dt><dd><span class="swatch" style="background:${H(s.color)}"></span>${H(s.color)}</dd>` : ""}
1860
+ </dl>
1861
+ ${(s.reasons || []).length ? `<div class="chips" style="margin-top:9px">
1862
+ ${s.reasons.map((r) => `<span class="chip">${H(r)}</span>`).join("")}</div>` : ""}
1863
+ ${s.ai && s.ai.scene ? `<div class="note" style="margin-top:9px">AI 判断:${H(s.ai.scene)}</div>` : ""}
1864
+ </div>`)}
1865
+ ${pane("grade", `
1866
+ ${actionDeckHtml({ chips: deckChipsHtml(), result: candHtml, prompt: promptBox })}
1867
+ <div class="card" id="insp-grade">
1868
+ <div class="card-h"><span class="t">手动胶片预设与影调</span><span class="sp"></span>
1869
+ <button class="btn btn-sm ghost" onclick="resetGrade()">归零</button></div>
1777
1870
  <div class="preset-grid">${presets}</div>
1778
1871
  <details id="grade-adjustments" ${S.gradeAdjustOpen ? "open" : ""}>
1779
1872
  <summary class="note" style="cursor:pointer;margin-top:12px">手动微调 · ${g.order.length} 项</summary>
1780
1873
  <div class="sliders">
1781
- <div class="grp">影调</div>${group("影调")}
1782
- <div class="grp">色彩</div>${group("色彩")}
1783
- <div class="grp">质感</div>${group("质感")}
1874
+ <div class="grp">影调</div>${group("影调")}
1875
+ <div class="grp">色彩</div>${group("色彩")}
1876
+ <div class="grp">质感</div>${group("质感")}
1784
1877
  </div>
1785
1878
  </details>
1786
- <div class="note" id="grade-summary" style="margin-top:8px">${H(s.grade_summary || "")}</div>
1787
- </div>
1788
- <div class="card ai" id="insp-grade-ai">
1789
- <div class="card-h"><span class="t" style="color:var(--navy)">AI 调色方向与方案</span><span class="sp"></span>
1790
- <span class="s">定制方案</span></div>
1791
- ${copilotHtml()}
1792
- <p class="note" style="margin-bottom:9px">在上方选择处理范围并生成方案;候选返回后再预览或采用。</p>
1793
- ${candHtml}
1794
- </div>
1795
- ${promptBox}
1796
- <div class="ai-out"></div>`)}
1797
- ${pane("retouch", `
1798
- <div class="card ai" id="insp-edit">
1799
- <div class="card-h"><span class="t" style="color:var(--navy)">指令修图</span><span class="sp"></span>
1800
- <span class="s">重绘像素 · 需人工验收</span></div>
1801
- <div class="note" style="margin-bottom:8px">
1802
- 点击下方预设只会填入指令,<b>不会自动开修</b>;确认后点击上方唯一的「开始修图」。
1803
- </div>
1804
- <div class="row" style="margin-bottom:9px">${editPresets}</div>
1805
- <textarea class="inst" id="inst" placeholder="选择上方预设填入指令,或直接输入修图需求(例如:去掉画面右侧的游客)">${H(S.editInstruction || "")}</textarea>
1806
- ${imageSizeControlsHtml("edit")}
1807
- </div>
1808
- ${editResultCard(s)}
1809
- ${promptBox}
1810
- <div class="ai-out"></div>`)}
1811
- ${pane("finish", `
1812
- <div class="ai-out"></div>
1813
- <div class="card" id="insp-finish">
1814
- <div class="card-h"><span class="t">出品台</span><span class="sp"></span>
1815
- <span class="s">使用当前精修成品</span></div>
1816
- ${finishSourcesHtml()}
1879
+ <div class="note" id="grade-summary" style="margin-top:8px">${H(s.grade_summary || "")}</div>
1880
+ </div>`)}
1881
+ ${pane("retouch", `
1882
+ ${actionDeckHtml({ chips: `
1883
+ <div class="note" style="margin-bottom:7px">点预设只会把指令填进下面的框,<b>不会自动开修</b>;确认后再点「开始修图」。重绘像素,需人工验收。</div>
1884
+ <div class="row" style="margin-bottom:8px">${editPresets}</div>`,
1885
+ result: editResultCard(s), prompt: promptBox })}
1886
+ ${imageSizeControlsHtml("edit")}`)}
1887
+ ${pane("finish", `
1888
+ ${actionDeckHtml({ chips: finishSourcesHtml(), result: `<div id="finish-out"></div>`, prompt: promptBox })}
1889
+ <div class="card" id="insp-art">
1890
+ <div class="card-h"><span class="t">AI 艺术出片</span><span class="sp"></span>
1891
+ <span class="s">选风格 → 点操作台的「生成所选作品」</span></div>
1892
+ <div class="note" style="margin-bottom:9px">
1893
+ 选择一种风格并填写可选标题,再点操作台的「生成所选作品」。
1894
+ </div>
1895
+ <div class="art-grid" style="grid-template-columns:1fr 1fr">${artCards}</div>
1896
+ <div class="row" style="margin-top:9px">
1897
+ <input id="art-title" data-output-text value="${H(S.artTitle)}" placeholder="可选:指定标题 / 短句(留空则由照片决定)" style="flex:1;min-width:0;padding:9px;border:1px solid var(--line-2);border-radius:6px">
1898
+ </div>
1899
+ ${imageSizeControlsHtml("art")}
1817
1900
  <label style="display:flex;align-items:center;gap:8px;margin:14px 0 6px;cursor:pointer">
1818
1901
  <input id="output-no-text" type="checkbox" ${S.data.preferences?.no_text ? "checked" : ""} onchange="setOutputNoText(this.checked)">
1819
1902
  <b>不加文字</b>
1820
1903
  </label>
1821
- <p class="note" style="margin-bottom:12px">新生成的衍生作品不添加标题、字幕、日期或装饰字;原照片和地图底图自带的文字不擦除。已有作品需重新生成。</p>
1822
- <div class="sec-title" style="margin-top:16px">AI 艺术出片</div>
1823
- <div class="note" style="margin-bottom:9px">
1824
- 选择一种风格并填写可选标题,再生成这一张衍生作品。AI 来源遵循本页顶部选择。
1825
- </div>
1826
- <div class="art-grid" style="grid-template-columns:1fr 1fr">${artCards}</div>
1827
- <div class="row" style="margin-top:9px">
1828
- <input id="art-title" data-output-text value="${H(S.artTitle)}" placeholder="可选:指定标题 / 短句(留空则由照片决定)" style="flex:1;min-width:0;padding:9px;border:1px solid var(--line-2);border-radius:6px">
1829
- </div>
1830
- ${imageSizeControlsHtml("art")}
1831
- <div class="sec-title" style="margin-top:16px">本地画板<span class="n">即刻生成 · 不调用模型</span></div>
1832
- <select id="local-product" aria-label="本地衍生品类型" style="width:100%;padding:8px;border:1px solid var(--line-2);border-radius:6px">
1833
- ${[["postcard","手撕纸明信片"],["watercolor","写意水墨水彩"],["zine","Zine 纸刊"],["poster","杂志海报"],
1834
- ["film","电影剧照卡"],["polaroid","复古拍立得"],["grid9","仅生成九宫格"],["map","高德足迹地图"]].map(([k,t]) =>
1835
- `<option value="${k}" ${(S.localProduct || "postcard") === k ? "selected" : ""}>${t}</option>`).join("")}
1836
- </select>
1837
- <div class="row" style="margin-top:8px">
1838
- <button class="btn btn-sm ghost" onclick="openProducts()">标题与排版设置</button></div>
1839
- <p class="note" id="local-product-note" style="margin-top:7px"></p>
1840
- <div class="sec-title" style="margin-top:16px">已生成</div>
1841
- <div id="finish-out" style="margin-top:9px"></div>
1842
- </div>`)}
1843
- ${pane("caption", `
1844
- <div class="card" id="insp-caption">
1845
- <div class="card-h"><span class="t">朋友圈文案与标题</span></div>
1846
- <p class="note" style="margin-bottom:10px">点击上方「生成 3 版文案」。生成后可逐条复制;作品标题在出片页的排版设置中管理。</p>
1847
- <div id="captions"></div>
1904
+ <p class="note">新生成的衍生作品不添加标题、字幕、日期或装饰字;原照片和地图底图自带的文字不擦除。已有作品需重新生成。</p>
1848
1905
  </div>
1906
+ <div class="card" id="insp-local">
1907
+ <details>
1908
+ <summary class="card-h" style="cursor:pointer;display:flex;align-items:center">
1909
+ <span class="t">本地画板</span><span class="sp" style="flex:1"></span>
1910
+ <span class="s">即刻生成 · 不调用模型</span></summary>
1911
+ <select id="local-product" aria-label="本地衍生品类型" style="width:100%;margin-top:9px;padding:8px;border:1px solid var(--line-2);border-radius:6px">
1912
+ ${[["postcard","手撕纸明信片"],["watercolor","写意水墨水彩"],["zine","Zine 纸刊"],["poster","杂志海报"],
1913
+ ["film","电影剧照卡"],["polaroid","复古拍立得"],["grid9","仅生成九宫格"],["map","高德足迹地图"]].map(([k,t]) =>
1914
+ `<option value="${k}" ${(S.localProduct || "postcard") === k ? "selected" : ""}>${t}</option>`).join("")}
1915
+ </select>
1916
+ <div class="row" style="margin-top:8px">
1917
+ <button class="btn btn-sm ghost" onclick="openProducts()">标题与排版设置</button></div>
1918
+ <p class="note" id="local-product-note" style="margin-top:7px"></p>
1919
+ </details>
1920
+ </div>`)}
1921
+ ${pane("caption", `
1922
+ ${actionDeckHtml({ result: `<div id="captions"></div>`, prompt: promptBox })}
1849
1923
  <div class="card" id="insp-publish">
1850
1924
  <div class="card-h"><span class="t">确认后打包</span><span class="s">只保存到本机</span></div>
1851
1925
  <label style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
@@ -1858,23 +1932,22 @@ function renderInspector(passive = true) {
1858
1932
  <p>检查导出 JPEG 是否还含 EXIF 定位;地图上的可见地点需另行确认。</p>
1859
1933
  <button class="btn btn-sm" onclick="checkExportPrivacy()">检查导出包</button>
1860
1934
  <div id="privacy-result" class="note" role="status"></div></div>
1935
+ </div>`)}
1861
1936
  </div>
1862
- ${promptBox}
1863
- <div class="ai-out"></div>`)}
1864
- </div>
1865
- ${agentWorkflowHtml()}
1866
- ${stepFoot}`;
1867
-
1868
- wireInspector();
1869
- wireCopilot();
1870
- wireWorkflow();
1871
- renderCaptions();
1872
- syncExecutor();
1873
- renderFinishPane();
1874
- const output = outBox();
1875
- if (!useAgentModel() && output && S.outputs[S.step] && (!S.outputSlots[S.step] || S.outputSlots[S.step] === S.sel))
1876
- output.innerHTML = S.outputs[S.step];
1877
- box.scrollTop = scroll;
1937
+ ${stepFoot}`;
1938
+
1939
+ wireInspector();
1940
+ wireDeck();
1941
+ wireDeckControls();
1942
+ wireCopilot();
1943
+ renderCaptions();
1944
+ syncExecutor();
1945
+ renderFinishPane();
1946
+ const output = outBox();
1947
+ if (output && S.outputs[S.step] && (!S.outputSlots[S.step] || S.outputSlots[S.step] === S.sel))
1948
+ output.innerHTML = S.outputs[S.step];
1949
+ updateLocalProductState();
1950
+ box.scrollTop = scroll;
1878
1951
  }
1879
1952
 
1880
1953
 
@@ -1915,11 +1988,7 @@ function wireInspector() {
1915
1988
  if (size) size.addEventListener("change", () => { S[kind + "Size"] = size.value; });
1916
1989
  }
1917
1990
  updateLocalProductState();
1918
- const inst = $("inst");
1919
- if (inst) inst.addEventListener("input", () => {
1920
- S.editInstruction = inst.value;
1921
- rememberEditDraft();
1922
- });
1991
+ // The 修图指令 box is the deck's #deck-intent now; wireDeck() binds it once.
1923
1992
  const title = $("art-title");
1924
1993
  if (title) title.addEventListener("input", () => { S.artTitle = title.value; });
1925
1994
  const promptBox = $("prompt-box");
@@ -1977,9 +2046,12 @@ function wireInspector() {
1977
2046
  el.addEventListener("click", () => {
1978
2047
  const p = (S.data.ai.edit_presets || []).find((x) => x.key === el.dataset.edit);
1979
2048
  S.editPreset = el.dataset.edit;
1980
- S.editInstruction = p ? p.instruction : "";
1981
- rememberEditDraft();
1982
- $("inst").value = S.editInstruction;
2049
+ S.editInstruction = p ? p.instruction : "";
2050
+ S.copilotIntent = S.editInstruction;
2051
+ (S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = S.editInstruction;
2052
+ rememberEditDraft();
2053
+ const box = $("deck-intent");
2054
+ if (box) box.value = S.editInstruction;
1983
2055
  document.querySelectorAll("[data-edit]").forEach((b) => b.classList.remove("on"));
1984
2056
  el.classList.add("on");
1985
2057
  });
@@ -2015,8 +2087,8 @@ function renderCaptions() {
2015
2087
  const c = S.data.captions || {};
2016
2088
  const vs = c.versions || [];
2017
2089
  if (!vs.length) {
2018
- box.innerHTML = `<div class="note">还没有文案。点上面的「AI 生成」,模型会依据选片时记录的真实场景写 3 版,
2019
- 不会像旧版那样塞一段和照片无关的模板文字。</div>`;
2090
+ box.innerHTML = `<div class="note">还没有文案。点操作台的「生成 3 版文案」,模型会依据选片时记录的真实场景写 3 版,
2091
+ 不会像旧版那样塞一段和照片无关的模板文字。(这一帧的 Agent 轨道会把文案直接登记到这里。)</div>`;
2020
2092
  return;
2021
2093
  }
2022
2094
  box.innerHTML = (c.narrative ? `<div class="note" style="margin-bottom:10px;font-style:italic">
@@ -2837,7 +2909,7 @@ function promptCtx(action, opts) {
2837
2909
  if (action === "edit") {
2838
2910
  ctx.preset = S.editPreset || "";
2839
2911
  ctx.image_size = S.editSize || "auto";
2840
- const inst = $("inst");
2912
+ const inst = $("deck-intent");
2841
2913
  if (inst && inst.value.trim()) ctx.instruction = inst.value.trim();
2842
2914
  }
2843
2915
  if (s) ctx.photo = s.source;
@@ -2963,39 +3035,47 @@ async function aiTitle() {
2963
3035
  await aiRun("title");
2964
3036
  }
2965
3037
 
2966
- async function aiEdit() {
2967
- const inst = $("inst");
2968
- const text = inst ? inst.value.trim() : "";
2969
- if (!text) { toast("先写一句修图指令,或点一个预设按钮", "err"); return; }
2970
- S.editInstruction = text;
2971
- rememberEditDraft();
2972
- await aiRun("edit");
2973
- }
2974
-
2975
- function fillInstruction() {
2976
- const p = (S.data.ai.edit_presets || []).find((x) => x.key === S.editPreset);
2977
- if (!p) { toast("先在左侧点一个修图预设", "err"); return; }
2978
- S.editInstruction = p.instruction;
2979
- $("inst").value = p.instruction;
2980
- }
2981
-
2982
- function outBox() {
2983
- // One output region PER STEP. All six panes used to share a single duplicate
2984
- // `ai-out` id, so getElementById() always returned the SELECT pane's box: a
2985
- // result produced on 调色/修图/出片/文案 was painted into a hidden pane and the
2986
- // user saw nothing at all — "点了分析三套配方,前端工作台没显示".
2987
- const step = S.step || "";
2988
- return document.querySelector(`[data-pane="${step}"] .ai-out`)
2989
- || document.querySelector(`[data-pane="${step}"] #ai-out`)
2990
- || document.querySelector(".ai-out");
2991
- }
2992
-
2993
- function renderAiOut(html, step = S.step, slot = ["grade", "crop", "retouch"].includes(step) ? S.sel : null) {
2994
- S.outputs[step] = html;
2995
- S.outputSlots[step] = slot;
2996
- if (useAgentModel() || step !== S.step || (slot && slot !== S.sel)) return;
2997
- const box = outBox();
2998
- if (box) box.innerHTML = html;
3038
+ async function aiEdit() {
3039
+ // The 修图指令 input IS the deck's input now (one box per step, not two).
3040
+ const inst = $("deck-intent");
3041
+ const text = inst ? inst.value.trim() : (S.editInstruction || "");
3042
+ if (!text) { toast("先写一句修图指令,或点一个预设按钮", "err"); return; }
3043
+ S.editInstruction = text;
3044
+ S.copilotIntent = text;
3045
+ rememberEditDraft();
3046
+ await aiRun("edit");
3047
+ }
3048
+
3049
+ function fillInstruction() {
3050
+ const p = (S.data.ai.edit_presets || []).find((x) => x.key === S.editPreset);
3051
+ if (!p) { toast("先在左侧点一个修图预设", "err"); return; }
3052
+ S.editInstruction = p.instruction;
3053
+ S.copilotIntent = p.instruction;
3054
+ (S.copilotDrafts = S.copilotDrafts || {})[copilotDraftKey()] = p.instruction;
3055
+ const box = $("deck-intent");
3056
+ if (box) box.value = p.instruction;
3057
+ rememberEditDraft();
3058
+ }
3059
+
3060
+ function outBox() {
3061
+ // One output region PER STEP, now living inside that step's action deck. All six
3062
+ // panes once shared a single duplicate `ai-out` id, so getElementById() always
3063
+ // returned the SELECT pane's box: a result produced on 调色/修图/出片/文案 was
3064
+ // painted into a hidden pane and the user saw nothing at all.
3065
+ const step = S.step || "";
3066
+ return document.getElementById(`ai-out-${step}`)
3067
+ || document.querySelector(`[data-pane="${step}"] .ai-out`)
3068
+ || document.querySelector(".ai-out");
3069
+ }
3070
+
3071
+ function renderAiOut(html, step = S.step, slot = ["grade", "crop", "retouch"].includes(step) ? S.sel : null) {
3072
+ S.outputs[step] = html;
3073
+ S.outputSlots[step] = slot;
3074
+ // No model-source guard: a result is a result. Skipping the paint on the
3075
+ // agent-model track is why 「Agent 说得挺好,页面上什么都没有」 was possible.
3076
+ if (step !== S.step || (slot && slot !== S.sel)) return;
3077
+ const box = outBox();
3078
+ if (box) box.innerHTML = html;
2999
3079
  }
3000
3080
 
3001
3081
  /* ---------------- executor switch ---------------- */
@@ -3186,7 +3266,7 @@ function renderConfigPanels(c) {
3186
3266
 
3187
3267
  $("models").innerHTML = models.length
3188
3268
  ? models.map((m) => `<span class="chip" style="font-family:var(--mono)">${H(m)}</span>`).join("")
3189
- : `<span class="note">还没探测过。点上面的「探测连通性与在售模型」拉取真实列表。</span>`;
3269
+ : `<span class="note">还没探测过。点「探测连通性与在售模型」拉取真实列表。</span>`;
3190
3270
 
3191
3271
  const cm = c.custom_models || {};
3192
3272
  const keys = Object.keys(cm);
package/README.md CHANGED
@@ -181,10 +181,15 @@ trip-pulse/
181
181
  - 出片页明确显示「原片处理版 / 已采用修图」。存在待采用修图时,先要求采用或拒绝,
182
182
  不再静默使用原片生成。采用后仍需重新生成海报/拼图或重新打包;旧产物不会自动更新。
183
183
 
184
- - 每步只保留上方一个主要执行入口;裁切/调色用「当前格 / 全部已选」切换范围。
185
- 方向预设只填写要求,不会自动调用模型;修图只保留一份指令输入。
186
- - 候选照片只在照片库展示。出片页分为导出包、艺术风格选择、本地衍生品和单一结果图库;
187
- 排版抽屉只设置标题/字幕/版式,不重复放生成按钮。
184
+ - 每一步的主流程收进同一张「本步操作台」:谁来做/范围 → 要求输入 → 触发按钮 →
185
+ 本轮提示词或发给 Agent 的简报 → 结果(含候选与 Agent 会话),自上而下一次完成,
186
+ 不再需要「在下面写要求、回顶部点按钮、再滚下去看结果」。手动控件(预设、微调、
187
+ 画幅、风格)降到操作台下方,只作手动替代路径。
188
+ - 要求只用一个输入框:网关轨道它是发出去的要求,Agent 轨道同一个框就是会话追问;
189
+ 裁切/调色用「当前格 / 全部已选」切换范围。方向预设只填写要求,不会自动调用模型。
190
+ - 修图页同一套流程:预设只填指令 → 「开始修图」 → 下方结果卡直接采用/拒绝。
191
+ - 出片页拆成四块:操作台(出片来源 + 触发 + 结果图库)、AI 艺术出片、本地画板(折叠)、
192
+ 打包仍在文案步确认之后。
188
193
  - 全组调色支持先预览再采用;过期配方及已取消网关任务的返回结果不再自动落盘。
189
194
  Agent 的取消会转发停止请求;它在取消前已执行的修改仍需按需撤回。
190
195
  - 新采用的修图版本保存稳定副本;新的待采用结果与当前已采用版本分开,可明确采用或拒绝。
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.2';
19
+ const VERSION = '1.2.0';
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.2",
3
+ "version": "1.2.0",
4
4
  "description": "AI 朋友圈旅行出图助手 (trip-moments skill) — 工业级全流程旅行摄影智能出图与朋友圈九宫格套件",
5
5
  "main": "bin/install.js",
6
6
  "bin": {